diff --git a/app/controllers/course/external_assessment_imports_controller.rb b/app/controllers/course/external_assessment_imports_controller.rb new file mode 100644 index 0000000000..71aec6d236 --- /dev/null +++ b/app/controllers/course/external_assessment_imports_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +class Course::ExternalAssessmentImportsController < Course::ComponentController + Service = Course::Gradebook::ExternalAssessmentImportService + + def preview + authorize! :manage_gradebook_weights, current_course + @result = build_service.preview + render 'preview' + rescue Service::ImportError => e + render json: { errors: e.payload }, status: :unprocessable_entity + end + + def create + authorize! :manage_gradebook_weights, current_course + @summary = build_service.commit(on_conflict: params[:onConflict]) + render 'create' + rescue Service::ImportError => e + render json: { errors: e.payload }, status: :unprocessable_entity + end + + private + + def component + current_component_host[:course_gradebook_component] + end + + def build_service + Service.new( + course: current_course, + actor: current_user, + components: import_params[:components].map do |c| + { name: c[:name], weightage: c[:weightage].to_i, maximum_grade: c[:maximumGrade].to_f } + end, + identifier_mode: import_params[:identifierMode], + csv_data: import_params[:csvData] + ) + end + + def import_params + params.permit( + :identifierMode, :csvData, :onConflict, + components: [:name, :weightage, :maximumGrade] + ) + end +end diff --git a/app/controllers/course/external_assessments_controller.rb b/app/controllers/course/external_assessments_controller.rb new file mode 100644 index 0000000000..ef76110a40 --- /dev/null +++ b/app/controllers/course/external_assessments_controller.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true +class Course::ExternalAssessmentsController < Course::ComponentController + before_action :load_external_assessment, only: [:update, :destroy, :grades] + + def create + authorize! :manage_gradebook_weights, current_course + @external_assessment = Course::ExternalAssessment.create_for_course!( + course: current_course, + title: create_params[:title], + maximum_grade: create_params[:maximumGrade] + ) + render 'create' + rescue ActiveRecord::RecordInvalid => e + render json: { errors: { base: e.message } }, status: :unprocessable_entity + end + + def update + authorize! :manage_gradebook_weights, current_course + @external_assessment.update!(update_params_attrs) + render 'update' + rescue ActiveRecord::RecordInvalid => e + render json: { errors: { base: e.message } }, status: :unprocessable_entity + end + + def destroy + authorize! :manage_gradebook_weights, current_course + @external_assessment.destroy! + head :ok + end + + def grades + authorize! :grade, @external_assessment + course_user = current_course.course_users.find(grade_params[:courseUserId]) + @grade = @external_assessment.external_assessment_grades. + find_or_initialize_by(course_user: course_user) + @grade.grade = normalized_grade(grade_params[:grade]) + @grade.save! + render 'update_grade' + rescue ActiveRecord::RecordNotUnique + retry + rescue ActiveRecord::RecordNotFound + head :not_found + rescue ActiveRecord::RecordInvalid => e + render json: { errors: { base: e.message } }, status: :unprocessable_entity + end + + private + + def component + current_component_host[:course_gradebook_component] + end + + def load_external_assessment + @external_assessment = Course::ExternalAssessment.for_course(current_course).find(params[:id]) + rescue ActiveRecord::RecordNotFound + head :not_found + end + + def create_params + params.permit(:title, :maximumGrade) + end + + def update_params_attrs + attrs = {} + attrs[:title] = params[:title] if params.key?(:title) + attrs[:maximum_grade] = params[:maximumGrade] if params.key?(:maximumGrade) + attrs + end + + def grade_params + params.permit(:courseUserId, :grade) + end + + # Blank cell clears the grade to null (ungraded), never zero (decision #7). + def normalized_grade(value) + value.blank? ? nil : value + end +end diff --git a/app/controllers/course/gradebook_controller.rb b/app/controllers/course/gradebook_controller.rb index b5ef6c71c2..a5d250602a 100644 --- a/app/controllers/course/gradebook_controller.rb +++ b/app/controllers/course/gradebook_controller.rb @@ -16,6 +16,7 @@ def index student_ids: @students.map(&:user_id), assessment_ids: assessment_ids ) + load_externals end end end @@ -83,6 +84,15 @@ def fetch_categories_and_tabs [tabs.map(&:category).uniq(&:id), tabs] end + def load_externals + @external_assessments = Course::ExternalAssessment.for_course(current_course). + includes(:gradebook_contribution, external_assessment_grades: :course_user).to_a + @external_grades = @external_assessments.flat_map(&:external_assessment_grades) + @external_contributions = @external_assessments. + index_by(&:id). + transform_values(&:gradebook_contribution) + end + def fetch_students current_course.levels.to_a current_course.course_users.students.without_phantom_users. diff --git a/app/models/components/course/gradebook_ability_component.rb b/app/models/components/course/gradebook_ability_component.rb index d54a56cca6..7c6d0990d6 100644 --- a/app/models/components/course/gradebook_ability_component.rb +++ b/app/models/components/course/gradebook_ability_component.rb @@ -6,6 +6,7 @@ def define_permissions can :read_gradebook, Course, id: course.id if course_user&.staff? can :manage_gradebook_weights, Course, id: course.id if course_user&.manager_or_owner? can :manage_gradebook_settings, Course, id: course.id if course_user&.manager_or_owner? + can :grade, Course::ExternalAssessment if course_user&.teaching_staff? super end end diff --git a/app/models/course.rb b/app/models/course.rb index 7952e5a750..f52778fa95 100644 --- a/app/models/course.rb +++ b/app/models/course.rb @@ -55,6 +55,8 @@ class Course < ApplicationRecord has_many :assessments, through: :assessment_categories has_many :gradebook_contributions, class_name: 'Course::Gradebook::Contribution', dependent: :destroy, inverse_of: :course + has_many :external_assessments, class_name: 'Course::ExternalAssessment', + inverse_of: :course, dependent: :destroy has_many :assessment_skills, class_name: 'Course::Assessment::Skill', dependent: :destroy has_many :assessment_skill_branches, class_name: 'Course::Assessment::SkillBranch', diff --git a/app/models/course/assessment/tab.rb b/app/models/course/assessment/tab.rb index f1305a645c..79bbb5452b 100644 --- a/app/models/course/assessment/tab.rb +++ b/app/models/course/assessment/tab.rb @@ -10,7 +10,6 @@ class Course::Assessment::Tab < ApplicationRecord has_many :assessments, class_name: 'Course::Assessment', dependent: :destroy, inverse_of: :tab has_one :gradebook_contribution, class_name: 'Course::Gradebook::Contribution', dependent: :destroy, inverse_of: :tab - has_many :folders, class_name: 'Course::Material::Folder', through: :assessments, inverse_of: nil diff --git a/app/models/course/external_assessment.rb b/app/models/course/external_assessment.rb new file mode 100644 index 0000000000..565c968efa --- /dev/null +++ b/app/models/course/external_assessment.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +# A gradebook component graded outside Coursemology (e.g. a midterm or final). +# It is a first-class gradebook contributor, NOT a Course::Assessment: it never +# touches attempts, EXP, statistics, todos, or the lesson plan. Its weight lives on +# its course_gradebook_contributions row; its display grouping is synthesised by the +# gradebook serializer (no real tab/category exists). +class Course::ExternalAssessment < ApplicationRecord + # Sentinel id for the serializer's synthetic "External Assessments" category. + # Native categories are positive; externals and their synthetic grouping are negative. + SYNTHETIC_CATEGORY_ID = -1 + SYNTHETIC_CATEGORY_TITLE = 'External Assessments' + + validates :title, length: { maximum: 255 }, presence: true + validates :title, uniqueness: { scope: :course_id } + validates :maximum_grade, presence: true + validates :maximum_grade, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + validates :creator, presence: true + validates :updater, presence: true + + belongs_to :course, inverse_of: :external_assessments + has_one :gradebook_contribution, class_name: 'Course::Gradebook::Contribution', + inverse_of: :external_assessment, dependent: :destroy + has_many :external_assessment_grades, class_name: 'Course::ExternalAssessmentGrade', + inverse_of: :external_assessment, dependent: :destroy + + scope :for_course, ->(course) { where(course_id: course.id) } + + # The negative serialized id used by the synthetic tab AND the leaf assessment. + def synthetic_tab_id + -id + end + + # Creates an external assessment and its gradebook contribution in one transaction. + # Raises ActiveRecord::RecordInvalid on a duplicate title within the course. + def self.create_for_course!(course:, title:, maximum_grade:, weight: 0) + transaction do + external = course.external_assessments.create!(title: title, maximum_grade: maximum_grade) + Course::Gradebook::Contribution.create!(course: course, external_assessment: external, + weight: weight, weight_mode: 'equal', keep_highest: 0) + external + end + end +end diff --git a/app/models/course/external_assessment_grade.rb b/app/models/course/external_assessment_grade.rb new file mode 100644 index 0000000000..70518c79c9 --- /dev/null +++ b/app/models/course/external_assessment_grade.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +# One external grade for a (external assessment, course_user). The binding key is +# course_user_id (the authoritative link to the person); imported_identifier is a +# non-authoritative snapshot of the email/Student ID used at import (audit + upsert +# mismatch detection), null for grades typed/edited inline. +class Course::ExternalAssessmentGrade < ApplicationRecord + validates :course_user, presence: true + validates :grade, numericality: true, allow_nil: true + validates :course_user_id, uniqueness: { scope: :external_assessment_id } + validates :creator, presence: true + validates :updater, presence: true + + belongs_to :external_assessment, class_name: 'Course::ExternalAssessment', + inverse_of: :external_assessment_grades + belongs_to :course_user, inverse_of: :external_assessment_grades +end diff --git a/app/models/course/gradebook/contribution.rb b/app/models/course/gradebook/contribution.rb index 734e8a6b00..fcad0bb792 100644 --- a/app/models/course/gradebook/contribution.rb +++ b/app/models/course/gradebook/contribution.rb @@ -8,13 +8,16 @@ class Course::Gradebook::Contribution < ApplicationRecord belongs_to :course, inverse_of: :gradebook_contributions belongs_to :tab, class_name: 'Course::Assessment::Tab', inverse_of: :gradebook_contribution, optional: true + belongs_to :external_assessment, class_name: 'Course::ExternalAssessment', + inverse_of: :gradebook_contribution, optional: true validates :creator, presence: true validates :updater, presence: true validates :weight, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 } validates :weight_mode, presence: true validates :keep_highest, numericality: { only_integer: true, greater_than_or_equal_to: 0 } - validates :tab_id, uniqueness: true + validates :tab_id, uniqueness: { allow_nil: true } + validates :external_assessment_id, uniqueness: { allow_nil: true } validate :exactly_one_contributor validate :course_matches_contributor # Bulk-upserts tab contributions and their per-assessment contributions for a course. @@ -27,13 +30,22 @@ class Course::Gradebook::Contribution < ApplicationRecord # @param updates [Array] each { tab_id:, weight:, weight_mode:, keep_highest:, # excluded_assessment_ids: [Integer], assessment_weights: [{ assessment_id:, weight: }] } def self.bulk_update(course:, updates:) + external_updates, tab_updates = updates.partition { |e| e[:tab_id].to_i < 0 } + course_tab_ids = course.assessment_tabs.pluck(:id).to_set - updates.each { |e| raise ActiveRecord::RecordNotFound unless course_tab_ids.include?(e[:tab_id]) } + tab_updates.each { |e| raise ActiveRecord::RecordNotFound unless course_tab_ids.include?(e[:tab_id]) } + + external_ids = external_updates.map { |e| -e[:tab_id] } + externals_by_id = course.external_assessments.where(id: external_ids).index_by(&:id) + external_updates.each { |e| raise ActiveRecord::RecordNotFound unless externals_by_id.key?(-e[:tab_id]) } - tabs_by_id = Course::Assessment::Tab.where(id: updates.map { |e| e[:tab_id] }). + tabs_by_id = Course::Assessment::Tab.where(id: tab_updates.map { |e| e[:tab_id] }). includes(:assessments).index_by(&:id) - transaction { updates.each { |entry| apply_entry(course, tabs_by_id, entry) } } + transaction do + tab_updates.each { |entry| apply_entry(course, tabs_by_id, entry) } + external_updates.each { |entry| apply_external_entry(course, externals_by_id[-entry[:tab_id]], entry) } + end end # @api private @@ -58,6 +70,16 @@ def self.apply_entry(course, tabs_by_id, entry) end private_class_method :apply_entry + # @api private + def self.apply_external_entry(course, external, entry) + contribution = find_or_initialize_by(external_assessment_id: external.id) + contribution.tab = nil + contribution.course = course + contribution.assign_attributes(weight: entry[:weight], weight_mode: 'equal', keep_highest: 0) + contribution.save! + end + private_class_method :apply_external_entry + # @api private def self.assessment_contribution_for(assessment) Course::Gradebook::AssessmentContribution.find_or_initialize_by(assessment_id: assessment.id) @@ -117,15 +139,19 @@ def self.validate_custom_assessment_weights_sum!(tab, entry, included_sum, inclu private - # Until the external-alignment design adds `external_assessment_id`, the only - # contributor is the tab, so "exactly one" reduces to "tab present". def exactly_one_contributor - errors.add(:tab, :blank) if tab_id.blank? + return if [tab_id, external_assessment_id].compact.size == 1 + + errors.add(:base, :exactly_one_contributor) end def course_matches_contributor - return if tab.nil? || course.nil? + return if course.nil? - errors.add(:course, :invalid) if tab.category.course_id != course_id + contributor_course_id = + if tab then tab.category.course_id + elsif external_assessment then external_assessment.course_id + end + errors.add(:course, :invalid) if contributor_course_id && contributor_course_id != course_id end end diff --git a/app/models/course_user.rb b/app/models/course_user.rb index 944fe25e09..6682fb1fac 100644 --- a/app/models/course_user.rb +++ b/app/models/course_user.rb @@ -50,6 +50,8 @@ class CourseUser < ApplicationRecord inverse_of: :course_user, dependent: :destroy has_many :groups, through: :group_users, class_name: 'Course::Group', source: :group has_many :personal_times, class_name: 'Course::PersonalTime', inverse_of: :course_user, dependent: :destroy + has_many :external_assessment_grades, class_name: 'Course::ExternalAssessmentGrade', + inverse_of: :course_user, dependent: :destroy belongs_to :reference_timeline, class_name: 'Course::ReferenceTimeline', inverse_of: :course_users, optional: true default_scope { where(deleted_at: nil) } diff --git a/app/services/course/gradebook/external_assessment_import_service.rb b/app/services/course/gradebook/external_assessment_import_service.rb new file mode 100644 index 0000000000..65128f89df --- /dev/null +++ b/app/services/course/gradebook/external_assessment_import_service.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true +require 'csv' + +class Course::Gradebook::ExternalAssessmentImportService # rubocop:disable Metrics/ClassLength + class ImportError < StandardError + attr_reader :payload + + def initialize(payload) + @payload = payload + super(payload.is_a?(Hash) ? payload[:message].to_s : payload.to_s) + end + end + + def initialize(course:, actor:, components:, identifier_mode:, csv_data:) + @course = course + @actor = actor + @components = components.map(&:symbolize_keys) + @identifier_mode = identifier_mode.to_s + @csv_data = csv_data + end + + def preview + rows = parsed_rows + resolution = resolve(rows) + { + ok: resolution[:unresolved].empty? && resolution[:malformed].empty?, + unresolved: resolution[:unresolved], + malformed: resolution[:malformed], + sample: sample(resolution[:resolved]), + conflicts: conflicts(resolution[:resolved]) + } + end + + def commit(on_conflict:) + rows = parsed_rows + resolution = resolve(rows) + unless resolution[:unresolved].empty? && resolution[:malformed].empty? + raise ImportError, { message: 'validation_failed', + unresolved: resolution[:unresolved], + malformed: resolution[:malformed] } + end + + summary = { createdComponents: 0, updatedComponents: 0, gradesWritten: 0 } + ActiveRecord::Base.transaction { write_components(summary, resolution[:resolved], on_conflict) } + summary + end + + private + + def write_components(summary, resolved, on_conflict) + @components.each do |component| + external = existing_external(component[:name]) + if external + summary[:updatedComponents] += 1 + summary[:gradesWritten] += upsert_grades(external, component, resolved, + on_conflict: on_conflict) + else + summary[:createdComponents] += 1 + summary[:gradesWritten] += create_component(component, resolved) + end + end + end + + def parsed_rows + guard_no_duplicate_components! + table = CSV.parse(@csv_data.to_s, headers: true) + guard_header!(table.headers) + table + end + + def guard_no_duplicate_components! + names = @components.map { |c| c[:name] } + return if names.uniq.length == names.length + + raise ImportError, { message: 'duplicate_component_name' } + end + + def guard_header!(headers) + expected = ['Identifier'] + @components.map { |c| c[:name] } + return if headers == expected + + raise ImportError, { message: 'bad_header', expected: expected, got: headers } + end + + def roster_lookup + @roster_lookup ||= + if @identifier_mode == 'email' + @course.course_users.includes(:user).index_by { |cu| cu.user.email.downcase } + else + @course.course_users.where.not(external_id: [nil, '']).index_by(&:external_id) + end + end + + def lookup_key(identifier) + (@identifier_mode == 'email') ? identifier.to_s.downcase : identifier.to_s + end + + def resolve(table) + resolved = [] + unresolved = [] + malformed = [] + table.each_with_index do |row, idx| + identifier = row['Identifier'].to_s.strip + course_user = roster_lookup[lookup_key(identifier)] + if course_user.nil? + unresolved << identifier + next + end + grades, bad = parse_grades(row, idx) + malformed.concat(bad) + resolved << { course_user: course_user, identifier: identifier, grades: grades } + end + { resolved: resolved, unresolved: unresolved.uniq, malformed: malformed } + end + + def parse_grades(row, row_idx) + grades = {} + malformed = [] + @components.each do |component| + raw = row[component[:name]] + if raw.nil? || raw.to_s.strip.empty? + grades[component[:name]] = nil + elsif numeric?(raw) + grades[component[:name]] = Float(raw) + else + malformed << "row #{row_idx + 2}, #{component[:name]}: #{raw}" + end + end + [grades, malformed] + end + + def numeric?(value) + Float(value) + true + rescue ArgumentError, TypeError + false + end + + def sample(resolved) + resolved.first(5).map do |r| + { studentName: r[:course_user].name, grades: r[:grades] } + end + end + + def conflicts(resolved) + result = [] + @components.each do |component| + conflicts_for_component(component, resolved, result) + end + result + end + + def conflicts_for_component(component, resolved, result) + external = existing_external(component[:name]) + return unless external + + grade_by_cu = external.external_assessment_grades.index_by(&:course_user_id) + resolved.each do |row| + conflict = conflict_entry(component[:name], row, grade_by_cu) + result << conflict if conflict + end + end + + def conflict_entry(component_name, row, grade_by_cu) + in_file = row[:grades][component_name] + return nil if in_file.nil? + + existing = grade_by_cu[row[:course_user].id] + return nil if existing.nil? || existing.grade.nil? + + { + component: component_name, + studentName: row[:course_user].name, + existingGrade: existing.grade.to_f, + inFileGrade: in_file, + identifierMismatch: existing.imported_identifier.present? && + existing.imported_identifier != row[:identifier] + } + end + + def existing_external(name) + @existing_externals ||= {} + @existing_externals[name] ||= Course::ExternalAssessment.for_course(@course).find_by(title: name) + end + + def create_component(component, resolved) + external = User.with_stamper(@actor) do + Course::ExternalAssessment.create_for_course!( + course: @course, title: component[:name], + maximum_grade: component[:maximum_grade], weight: component[:weightage] + ) + end + rows = resolved.filter_map do |r| + build_grade(external, r) unless r[:grades][component[:name]].nil? + end + bulk_insert(rows) + rows.size + end + + def upsert_grades(external, component, resolved, on_conflict:) + grade_by_cu = external.external_assessment_grades.index_by(&:course_user_id) + written = 0 + resolved.each do |row| + written += upsert_one(grade_by_cu, component, row, on_conflict) + end + written + end + + def upsert_one(grade_by_cu, component, row, on_conflict) + in_file = row[:grades][component[:name]] + return 0 if in_file.nil? + + existing = grade_by_cu[row[:course_user].id] + if existing.nil? + bulk_insert([build_grade(existing_external(component[:name]), row, value: in_file)]) + 1 + elsif on_conflict.to_s == 'replace' || existing.grade.nil? + User.with_stamper(@actor) do + existing.update!(grade: in_file, imported_identifier: row[:identifier]) + end + 1 + else + 0 + end + end + + def build_grade(external, resolved_row, value: nil) + Course::ExternalAssessmentGrade.new( + external_assessment: external, + course_user: resolved_row[:course_user], + grade: value.nil? ? resolved_row[:grades][external.title] : value, + imported_identifier: resolved_row[:identifier], + creator: @actor, updater: @actor + ) + end + + def bulk_insert(records) + return if records.empty? + + Course::ExternalAssessmentGrade.import(records, validate: true) + end +end diff --git a/app/views/course/external_assessment_imports/create.json.jbuilder b/app/views/course/external_assessment_imports/create.json.jbuilder new file mode 100644 index 0000000000..b8db877400 --- /dev/null +++ b/app/views/course/external_assessment_imports/create.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true +json.createdComponents @summary[:createdComponents] +json.updatedComponents @summary[:updatedComponents] +json.gradesWritten @summary[:gradesWritten] diff --git a/app/views/course/external_assessment_imports/preview.json.jbuilder b/app/views/course/external_assessment_imports/preview.json.jbuilder new file mode 100644 index 0000000000..ccbe5f3c8f --- /dev/null +++ b/app/views/course/external_assessment_imports/preview.json.jbuilder @@ -0,0 +1,15 @@ +# frozen_string_literal: true +json.ok @result[:ok] +json.unresolved @result[:unresolved] +json.malformed @result[:malformed] +json.sample @result[:sample] do |row| + json.studentName row[:studentName] + json.grades row[:grades] +end +json.conflicts @result[:conflicts] do |c| + json.component c[:component] + json.studentName c[:studentName] + json.existingGrade c[:existingGrade] + json.inFileGrade c[:inFileGrade] + json.identifierMismatch c[:identifierMismatch] +end diff --git a/app/views/course/external_assessments/_external_assessment.json.jbuilder b/app/views/course/external_assessments/_external_assessment.json.jbuilder new file mode 100644 index 0000000000..1ef536660f --- /dev/null +++ b/app/views/course/external_assessments/_external_assessment.json.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true +json.id(-external_assessment.id) +json.title external_assessment.title +json.tabId external_assessment.synthetic_tab_id +json.maxGrade external_assessment.maximum_grade.to_f +json.external true +json.gradebookExcluded false diff --git a/app/views/course/external_assessments/create.json.jbuilder b/app/views/course/external_assessments/create.json.jbuilder new file mode 100644 index 0000000000..bf9e3d9873 --- /dev/null +++ b/app/views/course/external_assessments/create.json.jbuilder @@ -0,0 +1,15 @@ +# frozen_string_literal: true +json.assessment do + json.partial! 'external_assessment', external_assessment: @external_assessment +end +json.tab do + json.id @external_assessment.synthetic_tab_id + json.title @external_assessment.title + json.categoryId Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID + json.gradebookWeight(@external_assessment.gradebook_contribution&.weight&.to_f || 0) + json.weightMode 'equal' +end +json.category do + json.id Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID + json.title Course::ExternalAssessment::SYNTHETIC_CATEGORY_TITLE +end diff --git a/app/views/course/external_assessments/update.json.jbuilder b/app/views/course/external_assessments/update.json.jbuilder new file mode 100644 index 0000000000..2c32850018 --- /dev/null +++ b/app/views/course/external_assessments/update.json.jbuilder @@ -0,0 +1,9 @@ +# frozen_string_literal: true +json.assessment do + json.partial! 'external_assessment', external_assessment: @external_assessment +end +json.tab do + json.id @external_assessment.synthetic_tab_id + json.title @external_assessment.title + json.categoryId Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID +end diff --git a/app/views/course/external_assessments/update_grade.json.jbuilder b/app/views/course/external_assessments/update_grade.json.jbuilder new file mode 100644 index 0000000000..f997450dfc --- /dev/null +++ b/app/views/course/external_assessments/update_grade.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true +json.studentId @grade.course_user.user_id +json.assessmentId(-@grade.external_assessment_id) +json.grade @grade.grade&.to_f diff --git a/app/views/course/gradebook/index.json.jbuilder b/app/views/course/gradebook/index.json.jbuilder index b3cc4ec4d0..435bb9f9ca 100644 --- a/app/views/course/gradebook/index.json.jbuilder +++ b/app/views/course/gradebook/index.json.jbuilder @@ -2,31 +2,65 @@ json.weightedViewEnabled @weighted_view_enabled json.canManageWeights can?(:manage_gradebook_weights, current_course) -json.categories @categories do |cat| - json.id cat.id - json.title cat.title +json.categories do + json.array!(@categories) do |cat| + json.id cat.id + json.title cat.title + end + if @external_assessments.any? + json.child! do + json.id Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID + json.title Course::ExternalAssessment::SYNTHETIC_CATEGORY_TITLE + end + end end -json.tabs @tabs do |tab| - json.id tab.id - json.title tab.title - json.categoryId tab.category_id - if @weighted_view_enabled - contribution = @tab_contributions[tab.id] - json.gradebookWeight (contribution&.weight || 0).to_f - json.weightMode(contribution&.weight_mode || 'equal') +json.tabs do + json.array!(@tabs) do |tab| + json.id tab.id + json.title tab.title + json.categoryId tab.category_id + if @weighted_view_enabled + contribution = @tab_contributions[tab.id] + json.gradebookWeight (contribution&.weight || 0).to_f + json.weightMode(contribution&.weight_mode || 'equal') + end + end + @external_assessments.each do |external| + json.child! do + json.id external.synthetic_tab_id + json.title external.title + json.categoryId Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID + if @weighted_view_enabled + contribution = @external_contributions[external.id] + json.gradebookWeight (contribution&.weight || 0).to_f + json.weightMode 'equal' + end + end end end -json.assessments @published_assessments do |assessment| - json.id assessment.id - json.title assessment.title - json.tabId assessment.tab_id - json.maxGrade @assessment_max_grades[assessment.id] || 0 - if @weighted_view_enabled - contribution = @assessment_contributions[assessment.id] - json.gradebookWeight contribution&.weight&.to_f - json.gradebookExcluded(contribution&.excluded || false) +json.assessments do + json.array!(@published_assessments) do |assessment| + json.id assessment.id + json.title assessment.title + json.tabId assessment.tab_id + json.maxGrade @assessment_max_grades[assessment.id] || 0 + if @weighted_view_enabled + contribution = @assessment_contributions[assessment.id] + json.gradebookWeight contribution&.weight&.to_f + json.gradebookExcluded(contribution&.excluded || false) + end + end + @external_assessments.each do |external| + json.child! do + json.id(-external.id) + json.title external.title + json.tabId external.synthetic_tab_id + json.maxGrade external.maximum_grade.to_f + json.external true + json.gradebookExcluded false if @weighted_view_enabled + end end end @@ -39,11 +73,21 @@ json.students @students do |course_user| json.totalXp course_user.experience_points end -json.submissions @submissions do |sub| - json.submissionId sub.submission_id - json.studentId sub.student_id - json.assessmentId sub.assessment_id - json.grade sub.grade&.to_f +json.submissions do + json.array!(@submissions) do |sub| + json.submissionId sub.submission_id + json.studentId sub.student_id + json.assessmentId sub.assessment_id + json.grade sub.grade&.to_f + end + @external_grades.each do |grade| + json.child! do + json.studentId grade.course_user.user_id + json.assessmentId(-grade.external_assessment_id) + json.grade grade.grade&.to_f + end + end end json.gamificationEnabled current_course.gamified? +json.userId current_user&.id diff --git a/client/app/api/course/Gradebook.ts b/client/app/api/course/Gradebook.ts index 7603f1f2a1..c855f9abe9 100644 --- a/client/app/api/course/Gradebook.ts +++ b/client/app/api/course/Gradebook.ts @@ -1,4 +1,13 @@ -import { GradebookData, UpdateWeightsPayload } from 'types/course/gradebook'; +import { + ExternalAssessmentNode, + ExternalAssessmentUpdate, + ExternalGradePayload, + GradebookData, + ImportCommitSummary, + ImportPreviewRequest, + ImportPreviewResult, + UpdateWeightsPayload, +} from 'types/course/gradebook'; import { APIResponse } from 'api/types'; @@ -18,4 +27,54 @@ export default class GradebookAPI extends BaseCourseAPI { ): APIResponse { return this.client.patch(`${this.#urlPrefix}/weights`, payload); } + + createExternal(payload: { + title: string; + maximumGrade: number; + }): APIResponse { + return this.client.post(`${this.#urlPrefix}/external_assessments`, payload); + } + + // `id` is the positive external id (negate the negative serialized id before calling). + updateExternal( + id: number, + payload: { title?: string; maximumGrade?: number }, + ): APIResponse { + return this.client.patch( + `${this.#urlPrefix}/external_assessments/${id}`, + payload, + ); + } + + deleteExternal(id: number): APIResponse { + return this.client.delete(`${this.#urlPrefix}/external_assessments/${id}`); + } + + setExternalGrade( + id: number, + payload: { studentId: number; grade: number | null }, + ): APIResponse { + return this.client.put( + `${this.#urlPrefix}/external_assessments/${id}/grades`, + payload, + ); + } + + importPreview( + payload: ImportPreviewRequest, + ): APIResponse { + return this.client.post( + `${this.#urlPrefix}/external_assessment_imports/preview`, + payload, + ); + } + + importCommit( + payload: ImportPreviewRequest & { onConflict: 'keep' | 'replace' }, + ): APIResponse { + return this.client.post( + `${this.#urlPrefix}/external_assessment_imports`, + payload, + ); + } } diff --git a/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsPrompt.test.tsx b/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsPrompt.test.tsx index cce652258c..937f3df231 100644 --- a/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsPrompt.test.tsx +++ b/client/app/bundles/course/gradebook/__tests__/ConfigureWeightsPrompt.test.tsx @@ -477,4 +477,35 @@ describe('per-assessment exclusion', () => { expect(screen.queryByText(/no weights set yet/i)).not.toBeInTheDocument(); }); }); + + describe('external assessment tab', () => { + it('shows an external-only tab and lets its weight be edited', () => { + render( + , + ); + + // The external tab row must appear with its title and weight input + expect(screen.getByText('External Assessments')).toBeInTheDocument(); + const weightInput = screen.getByLabelText('Midterm'); + expect(weightInput).toBeInTheDocument(); + expect(weightInput).not.toBeDisabled(); + expect(weightInput).toHaveValue(50); + }); + }); }); diff --git a/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx b/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx index c9f01f0dd0..c9f1d80b49 100644 --- a/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx +++ b/client/app/bundles/course/gradebook/__tests__/GradebookIndex.test.tsx @@ -223,6 +223,15 @@ describe('GradebookIndex', () => { expect(within(dialog).queryByText('Total XP')).not.toBeInTheDocument(); }); + it('renders the import external assessments button when manager and students exist', async () => { + render(, { state: populatedStateManagerWeightedOff }); + expect( + await screen.findByRole('button', { + name: /import external assessments/i, + }), + ).toBeInTheDocument(); + }); + describe('weighted-view discoverability hint', () => { it('shows the hint to managers when the weighted view is off', async () => { render(, { state: populatedStateManagerWeightedOff }); diff --git a/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx b/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx index 904d4513cb..c2411bcaf0 100644 --- a/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx +++ b/client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx @@ -2,6 +2,8 @@ import userEvent from '@testing-library/user-event'; import { store as appStore } from 'store'; import { render, screen, waitFor, within } from 'test-utils'; +import CourseAPI from 'api/course'; + import GradebookTable from '../components/GradebookTable'; import type { AssessmentData, @@ -11,6 +13,8 @@ import type { TabData, } from '../types'; +jest.mock('api/course'); + const categories: CategoryData[] = [{ id: 1, title: 'Cat A' }]; const tabs: TabData[] = [{ id: 10, title: 'Tab 1', categoryId: 1 }]; const assessments: AssessmentData[] = [ @@ -226,6 +230,13 @@ describe('GradebookTable', () => { expect(await screen.findByText('Max Marks')).toBeInTheDocument(); }); + it('hides the Max Marks header row when all assessment columns are deselected', async () => { + localStorage.setItem(STORAGE_KEY, JSON.stringify({ 'asn-100': false })); + renderTable(); + await screen.findByText('Alice'); + expect(screen.queryByText('Max Marks')).not.toBeInTheDocument(); + }); + it('renders row selection checkboxes', async () => { renderTableWithAssessmentVisible(); await screen.findByText('Alice'); @@ -737,6 +748,237 @@ describe('GradebookTable', () => { }); }); + describe('assessment grade cell rendering', () => { + const renderGradeCells = (subs: SubmissionData[]): void => { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ name: true, 'asn-100': true }), + ); + render( + , + { state: userState }, + ); + }; + + it('renders "—" when the student has no submission for an assessment', async () => { + renderGradeCells([]); + await screen.findByText('Alice'); + const aliceRow = screen.getByText('Alice').closest('tr')!; + expect( + within(aliceRow as HTMLElement).getByText('—'), + ).toBeInTheDocument(); + }); + + it('renders an empty cell (not "—") for a submission with a null grade', async () => { + // Alice: submission with null grade → empty; Bob: no submission → '—' + renderGradeCells([ + { submissionId: 1, studentId: 1, assessmentId: 100, grade: null }, + ]); + await screen.findByText('Alice'); + const aliceRow = screen.getByText('Alice').closest('tr')!; + expect( + within(aliceRow as HTMLElement).queryByText('—'), + ).not.toBeInTheDocument(); + const bobRow = screen.getByText('Bob').closest('tr')!; + expect(within(bobRow as HTMLElement).getByText('—')).toBeInTheDocument(); + }); + + it('renders the grade as a link to the submission when submissionId is present', async () => { + renderGradeCells([ + { submissionId: 42, studentId: 1, assessmentId: 100, grade: 7 }, + ]); + await screen.findByText('Alice'); + const aliceRow = screen.getByText('Alice').closest('tr')!; + expect( + within(aliceRow as HTMLElement).getByRole('link', { name: '7' }), + ).toBeInTheDocument(); + }); + }); + + describe('external assessment columns', () => { + const externalAssessments: AssessmentData[] = [ + { id: 100, title: 'Quiz 1', tabId: 10, maxGrade: 10 }, + { id: -5, title: 'Midterm', tabId: 200, maxGrade: 50, external: true }, + ]; + const externalTabs: TabData[] = [ + { id: 10, title: 'Tab 1', categoryId: 1 }, + { id: 200, title: 'Midterm', categoryId: 2 }, + ]; + const externalCategories: CategoryData[] = [ + { id: 1, title: 'Cat A' }, + { id: 2, title: 'External Assessments' }, + ]; + + const renderWithExternal = (): void => { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ name: true, 'asn--5': true, 'asn-100': true }), + ); + render( + , + { state: userState }, + ); + }; + + it('renders the External badge in the external column header', async () => { + renderWithExternal(); + expect(await screen.findByText('External')).toBeVisible(); + }); + + it('edits an external grade inline and persists optimistically', async () => { + (CourseAPI.gradebook.setExternalGrade as jest.Mock).mockResolvedValue({ + data: { studentId: 1, assessmentId: -5, grade: 45 }, + }); + renderWithExternal(); + const cell = await screen.findByText('30'); + await userEvent.click(cell); + const input = screen.getByRole('textbox', { + name: /midterm grade for alice/i, + }); + await userEvent.clear(input); + await userEvent.type(input, '45'); + await userEvent.tab(); + await waitFor(() => + expect(CourseAPI.gradebook.setExternalGrade).toHaveBeenCalledWith(5, { + studentId: 1, + grade: 45, + }), + ); + expect(await screen.findByText('45')).toBeVisible(); + }); + + it('rolls back the cell and keeps the old value when the API rejects', async () => { + (CourseAPI.gradebook.setExternalGrade as jest.Mock).mockRejectedValue( + new Error('boom'), + ); + renderWithExternal(); + const cell = await screen.findByText('30'); + await userEvent.click(cell); + const input = screen.getByRole('textbox', { + name: /midterm grade for alice/i, + }); + await userEvent.clear(input); + await userEvent.type(input, '45'); + await userEvent.tab(); + await waitFor(() => expect(screen.getByText('30')).toBeVisible()); + }); + + it('edits external max marks inline and persists', async () => { + (CourseAPI.gradebook.updateExternal as jest.Mock).mockResolvedValue({ + data: { + assessment: { + id: -5, + title: 'Midterm', + tabId: 200, + maxGrade: 80, + external: true, + }, + tab: { id: 200, title: 'Midterm', categoryId: 2 }, + }, + }); + renderWithExternal(); + const maxCell = await screen.findByText('/50'); + await userEvent.click(maxCell); + const input = screen.getByRole('textbox', { + name: /midterm max marks/i, + }); + await userEvent.clear(input); + await userEvent.type(input, '80'); + await userEvent.tab(); + await waitFor(() => + expect(CourseAPI.gradebook.updateExternal).toHaveBeenCalledWith(5, { + maximumGrade: 80, + }), + ); + }); + + it('keeps the regular column max-marks cell read-only', async () => { + renderWithExternal(); + // Quiz 1 max is /10 (regular). Clicking it must not produce a textbox. + const maxCell = await screen.findByText('/10'); + await userEvent.click(maxCell); + expect( + screen.queryByRole('textbox', { name: /quiz 1 max/i }), + ).not.toBeInTheDocument(); + }); + + it('keeps regular assessment cells read-only (no input on click)', async () => { + renderWithExternal(); + // Alice has no Quiz 1 submission → '—' is rendered (not an ExternalGradeCell). + // Clicking the '—' in the Quiz 1 column must NOT produce a textbox. + await screen.findByText('Midterm'); // wait for render + const dashCells = screen.getAllByText('—'); + // The first '—' in Alice's row is the Quiz 1 cell (no submission). + await userEvent.click(dashCells[0]); + expect( + screen.queryByRole('textbox', { name: /quiz 1/i }), + ).not.toBeInTheDocument(); + }); + + it('renames an external column from the header menu', async () => { + (CourseAPI.gradebook.updateExternal as jest.Mock).mockResolvedValue({ + data: { + assessment: { + id: -5, + title: 'Midterm Exam', + tabId: 200, + maxGrade: 50, + external: true, + }, + tab: { id: 200, title: 'Midterm Exam', categoryId: 2 }, + }, + }); + renderWithExternal(); + await userEvent.click( + await screen.findByRole('button', { name: /manage midterm/i }), + ); + await userEvent.click(screen.getByText('Rename')); + const dialog = screen.getByRole('dialog'); + const input = within(dialog).getByLabelText('Name'); + await userEvent.clear(input); + await userEvent.type(input, 'Midterm Exam'); + await userEvent.click(screen.getByText('Save')); + await waitFor(() => + expect(CourseAPI.gradebook.updateExternal).toHaveBeenCalledWith(5, { + title: 'Midterm Exam', + }), + ); + }); + + it('deletes an external column after confirmation', async () => { + (CourseAPI.gradebook.deleteExternal as jest.Mock).mockResolvedValue({ + data: undefined, + }); + renderWithExternal(); + await userEvent.click( + await screen.findByRole('button', { name: /manage midterm/i }), + ); + await userEvent.click(screen.getByText('Delete')); + await userEvent.click(screen.getByRole('button', { name: 'Delete' })); + await waitFor(() => + expect(CourseAPI.gradebook.deleteExternal).toHaveBeenCalledWith(5), + ); + }); + }); + describe('cross-page selection', () => { it('export label reflects selection count across pages', async () => { const user = userEvent.setup(); diff --git a/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx b/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx index 9bc42a310e..9032f573f4 100644 --- a/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx +++ b/client/app/bundles/course/gradebook/__tests__/GradebookWeightedTable.test.tsx @@ -180,6 +180,19 @@ describe('GradebookWeightedTable', () => { expect(screen.getByText('/70')).toBeInTheDocument(); }); + // 3b. Weight subheader switches to "{weight}% of grade" in percent mode + it('shows "{weight}% of grade" subheader for each non-excluded tab in percent mode', async () => { + const user = userEvent.setup(); + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 60), makeTab(11, 'Tab 2', 1, 40)], + }); + await user.click(screen.getByRole('radio', { name: /percentage/i })); + const thead = document.querySelector('thead')!; + const row3 = thead.querySelectorAll('tr')[2] as HTMLElement; + expect(within(row3).getByText('60% of grade')).toBeInTheDocument(); + expect(within(row3).getByText('40% of grade')).toBeInTheDocument(); + }); + // 4a. Total column shows "/100" when sum = 100 (points default) it('shows "/100" in total column header when weights sum to 100', () => { renderWeighted({ @@ -211,6 +224,18 @@ describe('GradebookWeightedTable', () => { ); }); + // 4d. Percent mode, weights ≠ 100 → total header shows "{weight}% total" warning + it('total column header shows "{weight}% total" warning in percent mode when sum ≠ 100', async () => { + const user = userEvent.setup(); + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 60), makeTab(11, 'Tab 2', 1, 20)], + }); + await user.click(screen.getByRole('radio', { name: /percentage/i })); + const thead = document.querySelector('thead')!; + const row3 = thead.querySelectorAll('tr')[2] as HTMLElement; + expect(within(row3).getByText('80% total')).toBeInTheDocument(); + }); + // 5. Cell renders subtotal × weight as points (not percentage); non-integer → 2dp it('renders cell as subtotal × weight in points (not a percentage); 2dp when non-integer', () => { // grade=130, maxGrade=150 → subtotal=130/150≈0.8667; weight=100 @@ -377,6 +402,20 @@ describe('GradebookWeightedTable', () => { expect(screen.getByText(/set your own/i)).toBeInTheDocument(); }); + // 10e. Showing default weights suppresses the projected-total policy hint + it('does not show the projected-total policy banner when default weights are in effect', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 0), makeTab(11, 'Tab 2', 1, 0)], + assessments: [ + makeAssessment(100, 'Quiz 1', 10, 150), + makeAssessment(101, 'Quiz 2', 11, 100), + ], + }); + expect( + screen.queryByText(/projected totals count ungraded assessments as 0/i), + ).not.toBeInTheDocument(); + }); + // 10a. Default split feeds the totals: two tabs → 50/100 each, summing to 100. it('applies an equal split (sums to 100) when no weights are configured', () => { renderWeighted({ @@ -411,6 +450,18 @@ describe('GradebookWeightedTable', () => { expect(screen.getByText(/no weights configured/i)).toBeInTheDocument(); }); + // 10f. Degenerate + canManageWeights=false → no-access copy + it('shows "No tab weights have been configured yet" when canManageWeights is false and no assessments', () => { + renderWeighted({ + tabs: [makeTab(10, 'Tab 1', 1, 0)], + assessments: [], + canManageWeights: false, + }); + expect( + screen.getByText(/no tab weights have been configured yet/i), + ).toBeInTheDocument(); + }); + // 10c. At least one non-zero weight → banner absent it('does not show empty-state banner when at least one tab has a non-zero weight', () => { renderWeighted({ @@ -1116,4 +1167,44 @@ describe('GradebookWeightedTable', () => { expect(cells[cells.length - 1]).toHaveTextContent('90%'); }); }); + + describe('external assessment regression', () => { + it('includes an external assessment in its tab subtotal and the projected total', () => { + // Regular tab (weight 50): Quiz id=100, max=10, Alice grade=8 + // subtotal = 8/10 = 0.8; cell = 0.8 × 50 = 40 + // External tab (weight 50): Midterm id=-5, max=50, Alice grade=25 + // subtotal = 25/50 = 0.5; cell = 0.5 × 50 = 25 + // Total = 40 + 25 = 65 + renderWeighted({ + categories: [ + makeCategory(1, 'Cat A'), + makeCategory(2, 'External Assessments'), + ], + tabs: [makeTab(10, 'Tab 1', 1, 50), makeTab(200, 'Midterm', 2, 50)], + assessments: [ + makeAssessment(100, 'Quiz 1', 10, 10), + { + id: -5, + title: 'Midterm', + tabId: 200, + maxGrade: 50, + external: true, + }, + ], + students: [makeStudent(1, 'Alice')], + submissions: [makeSub(1, 100, 8), makeSub(1, -5, 25)], + }); + + // External Assessments category and its tab must appear in the header + expect(screen.getByText('External Assessments')).toBeInTheDocument(); + expect(screen.getByText('Midterm')).toBeInTheDocument(); + + // External tab cell = 25 (integer); regular tab cell = 40; total = 65 + const cells = screen.getAllByRole('cell'); + const cellTexts = cells.map((c) => c.textContent?.trim()); + expect(cellTexts).toContain('40'); + expect(cellTexts).toContain('25'); + expect(cellTexts).toContain('65'); + }); + }); }); diff --git a/client/app/bundles/course/gradebook/__tests__/ImportExternalAssessmentsWizard.test.tsx b/client/app/bundles/course/gradebook/__tests__/ImportExternalAssessmentsWizard.test.tsx new file mode 100644 index 0000000000..dee3b63126 --- /dev/null +++ b/client/app/bundles/course/gradebook/__tests__/ImportExternalAssessmentsWizard.test.tsx @@ -0,0 +1,281 @@ +import userEvent from '@testing-library/user-event'; +import { store as appStore } from 'store'; +import { render, screen, waitFor } from 'test-utils'; + +import CourseAPI from 'api/course'; + +import ImportExternalAssessmentsWizard from '../components/import/ImportExternalAssessmentsWizard'; + +jest.mock('api/course'); +jest.mock('lib/components/wrappers/I18nProvider'); + +const renderWizard = (): void => { + render( + , + { store: appStore }, + ); +}; + +const file = (text: string): File => + new File([text], 'marks.csv', { type: 'text/csv' }); + +describe('ImportExternalAssessmentsWizard', () => { + beforeEach(() => jest.clearAllMocks()); + + it('walks define → upload → verify → commit with no conflicts', async () => { + (CourseAPI.gradebook.importPreview as jest.Mock).mockResolvedValue({ + data: { + ok: true, + unresolved: [], + malformed: [], + sample: [{ studentName: 'Alice', grades: { Midterm: 41 } }], + conflicts: [], + }, + }); + (CourseAPI.gradebook.importCommit as jest.Mock).mockResolvedValue({ + data: { createdComponents: 1, updatedComponents: 0, gradesWritten: 1 }, + }); + (CourseAPI.gradebook.index as jest.Mock).mockResolvedValue({ + data: { + categories: [], + tabs: [], + assessments: [], + students: [], + submissions: [], + gamificationEnabled: false, + weightedViewEnabled: true, + canManageWeights: true, + }, + }); + + renderWizard(); + // Step 1: type a component + await userEvent.type(screen.getByLabelText(/component name/i), 'Midterm'); + await userEvent.type(screen.getByLabelText(/weightage/i), '30'); + await userEvent.type(screen.getByLabelText(/max marks/i), '50'); + await userEvent.click(screen.getByRole('button', { name: /next/i })); + + // Step 2: upload + await userEvent.upload( + screen.getByLabelText(/upload/i), + file('Identifier,Midterm\nA001,41\n'), + ); + await userEvent.click(screen.getByRole('button', { name: /verify/i })); + + // Step 3: preview shows the sample + expect(await screen.findByText('Alice')).toBeVisible(); + await userEvent.click( + screen.getByRole('button', { name: /continue|confirm/i }), + ); + + await waitFor(() => + expect(CourseAPI.gradebook.importCommit).toHaveBeenCalled(), + ); + const payload = (CourseAPI.gradebook.importCommit as jest.Mock).mock + .calls[0][0]; + expect(payload.onConflict).toBe('replace'); + expect(payload.identifierMode).toBe('student_id'); + expect(payload.components[0]).toMatchObject({ + name: 'Midterm', + weightage: 30, + maximumGrade: 50, + }); + }); + + it('shows unresolved identifiers and stays on the upload step', async () => { + (CourseAPI.gradebook.importPreview as jest.Mock).mockResolvedValue({ + data: { + ok: false, + unresolved: ['ZZZ'], + malformed: [], + sample: [], + conflicts: [], + }, + }); + renderWizard(); + await userEvent.type(screen.getByLabelText(/component name/i), 'Midterm'); + await userEvent.type(screen.getByLabelText(/weightage/i), '30'); + await userEvent.type(screen.getByLabelText(/max marks/i), '50'); + await userEvent.click(screen.getByRole('button', { name: /next/i })); + await userEvent.upload( + screen.getByLabelText(/upload/i), + file('Identifier,Midterm\nZZZ,1\n'), + ); + await userEvent.click(screen.getByRole('button', { name: /verify/i })); + expect(await screen.findByText(/ZZZ/)).toBeVisible(); + expect(CourseAPI.gradebook.importCommit).not.toHaveBeenCalled(); + }); + + it('hides weightage field when weightedViewEnabled is false', async () => { + render( + , + { store: appStore }, + ); + await userEvent.type(screen.getByLabelText(/component name/i), 'Midterm'); + expect(screen.queryByLabelText(/weightage/i)).not.toBeInTheDocument(); + expect(screen.getByLabelText(/max marks/i)).toBeInTheDocument(); + }); + + it('disables Next when component name is empty', () => { + renderWizard(); + expect(screen.getByRole('button', { name: /next/i })).toBeDisabled(); + }); + + it('commits with keep when Keep Existing is clicked on the conflict prompt', async () => { + (CourseAPI.gradebook.importPreview as jest.Mock).mockResolvedValue({ + data: { + ok: true, + unresolved: [], + malformed: [], + sample: [{ studentName: 'Alice', grades: { Midterm: 20 } }], + conflicts: [ + { + component: 'Midterm', + studentName: 'Alice', + existingGrade: 10, + inFileGrade: 20, + identifierMismatch: false, + }, + ], + }, + }); + (CourseAPI.gradebook.importCommit as jest.Mock).mockResolvedValue({ + data: { createdComponents: 0, updatedComponents: 1, gradesWritten: 0 }, + }); + (CourseAPI.gradebook.index as jest.Mock).mockResolvedValue({ + data: { + categories: [], + tabs: [], + assessments: [], + students: [], + submissions: [], + gamificationEnabled: false, + weightedViewEnabled: true, + canManageWeights: true, + }, + }); + renderWizard(); + await userEvent.type(screen.getByLabelText(/component name/i), 'Midterm'); + await userEvent.type(screen.getByLabelText(/weightage/i), '30'); + await userEvent.type(screen.getByLabelText(/max marks/i), '50'); + await userEvent.click(screen.getByRole('button', { name: /next/i })); + await userEvent.upload( + screen.getByLabelText(/upload/i), + file('Identifier,Midterm\nA001,20\n'), + ); + await userEvent.click(screen.getByRole('button', { name: /verify/i })); + await userEvent.click( + await screen.findByRole('button', { name: /confirm import/i }), + ); + await userEvent.click( + await screen.findByRole('button', { name: /keep existing/i }), + ); + await waitFor(() => + expect( + (CourseAPI.gradebook.importCommit as jest.Mock).mock.calls[0][0] + .onConflict, + ).toBe('keep'), + ); + }); + + it('does not show Confirm import button when preview has errors', async () => { + (CourseAPI.gradebook.importPreview as jest.Mock).mockResolvedValue({ + data: { + ok: false, + unresolved: ['ZZZ'], + malformed: [], + sample: [], + conflicts: [], + }, + }); + renderWizard(); + await userEvent.type(screen.getByLabelText(/component name/i), 'Midterm'); + await userEvent.type(screen.getByLabelText(/weightage/i), '30'); + await userEvent.type(screen.getByLabelText(/max marks/i), '50'); + await userEvent.click(screen.getByRole('button', { name: /next/i })); + await userEvent.upload( + screen.getByLabelText(/upload/i), + file('Identifier,Midterm\nZZZ,1\n'), + ); + await userEvent.click(screen.getByRole('button', { name: /verify/i })); + await screen.findByText(/ZZZ/); + expect( + screen.queryByRole('button', { name: /confirm import/i }), + ).not.toBeInTheDocument(); + }); + + it('opens the conflict prompt and commits with keep/replace', async () => { + (CourseAPI.gradebook.importPreview as jest.Mock).mockResolvedValue({ + data: { + ok: true, + unresolved: [], + malformed: [], + sample: [{ studentName: 'Alice', grades: { Midterm: 20 } }], + conflicts: [ + { + component: 'Midterm', + studentName: 'Alice', + existingGrade: 10, + inFileGrade: 20, + identifierMismatch: false, + }, + ], + }, + }); + (CourseAPI.gradebook.importCommit as jest.Mock).mockResolvedValue({ + data: { createdComponents: 0, updatedComponents: 1, gradesWritten: 1 }, + }); + (CourseAPI.gradebook.index as jest.Mock).mockResolvedValue({ + data: { + categories: [], + tabs: [], + assessments: [], + students: [], + submissions: [], + gamificationEnabled: false, + weightedViewEnabled: true, + canManageWeights: true, + }, + }); + render( + , + { store: appStore }, + ); + // Step 1: 'Midterm' matches existing → max/weightage locked; just Next. + await userEvent.type(screen.getByLabelText(/component name/i), 'Midterm'); + await userEvent.click(screen.getByRole('button', { name: /next/i })); + await userEvent.upload( + screen.getByLabelText(/upload/i), + file('Identifier,Midterm\nA001,20\n'), + ); + await userEvent.click(screen.getByRole('button', { name: /verify/i })); + await userEvent.click( + await screen.findByRole('button', { name: /continue|confirm/i }), + ); + // conflict prompt + await userEvent.click( + await screen.findByRole('button', { name: /replace/i }), + ); + await waitFor(() => + expect( + (CourseAPI.gradebook.importCommit as jest.Mock).mock.calls[0][0] + .onConflict, + ).toBe('replace'), + ); + }); +}); diff --git a/client/app/bundles/course/gradebook/__tests__/buildAssessmentColumnIds.test.ts b/client/app/bundles/course/gradebook/__tests__/buildAssessmentColumnIds.test.ts new file mode 100644 index 0000000000..4c9520ef12 --- /dev/null +++ b/client/app/bundles/course/gradebook/__tests__/buildAssessmentColumnIds.test.ts @@ -0,0 +1,19 @@ +import { + buildAssessmentColumnId, + parseAssessmentColumnId, +} from '../components/buildAssessmentColumnIds'; + +describe('assessment column ids', () => { + it('round-trips a positive (regular) assessment id', () => { + expect(parseAssessmentColumnId(buildAssessmentColumnId(100))).toBe(100); + }); + + it('round-trips a negative (external) assessment id', () => { + expect(parseAssessmentColumnId(buildAssessmentColumnId(-5))).toBe(-5); + }); + + it('returns null for a non-assessment column id', () => { + expect(parseAssessmentColumnId('name')).toBeNull(); + expect(parseAssessmentColumnId('totalXp')).toBeNull(); + }); +}); diff --git a/client/app/bundles/course/gradebook/__tests__/buildTemplate.test.ts b/client/app/bundles/course/gradebook/__tests__/buildTemplate.test.ts new file mode 100644 index 0000000000..a0283fdfb9 --- /dev/null +++ b/client/app/bundles/course/gradebook/__tests__/buildTemplate.test.ts @@ -0,0 +1,45 @@ +import { buildTemplateCsv } from '../components/import/buildTemplate'; + +describe('buildTemplateCsv', () => { + it('builds a header of Identifier + component names in order', () => { + const csv = buildTemplateCsv([ + { name: 'Midterm', weightage: 30, maximumGrade: 50 }, + { name: 'Final', weightage: 50, maximumGrade: 100 }, + ]); + expect(csv.split('\n')[0]).toBe('Identifier,Midterm,Final'); + }); + + it('quotes a component name containing a comma', () => { + const csv = buildTemplateCsv([ + { name: 'Lab, week 1', weightage: 10, maximumGrade: 20 }, + ]); + expect(csv.split('\n')[0]).toBe('Identifier,"Lab, week 1"'); + }); + + it('returns "Identifier\\n" for empty components array', () => { + expect(buildTemplateCsv([])).toBe('Identifier\n'); + }); + + it('quotes a component name containing a double-quote', () => { + const csv = buildTemplateCsv([ + { name: 'My "Best" Quiz', weightage: 10, maximumGrade: 20 }, + ]); + expect(csv.split('\n')[0]).toBe('Identifier,"My ""Best"" Quiz"'); + }); + + it('quotes a component name containing a newline', () => { + const csv = buildTemplateCsv([ + { name: 'Lab\nWeek1', weightage: 10, maximumGrade: 20 }, + ]); + // The quoted cell spans two lines; verify the full header row content. + expect(csv.startsWith('Identifier,"Lab\nWeek1"')).toBe(true); + }); + + it('always ends with exactly one newline', () => { + const csv = buildTemplateCsv([ + { name: 'A', weightage: 0, maximumGrade: 100 }, + ]); + expect(csv.endsWith('\n')).toBe(true); + expect(csv.split('\n')).toHaveLength(2); // header line + empty string after trailing \n + }); +}); diff --git a/client/app/bundles/course/gradebook/__tests__/store.test.ts b/client/app/bundles/course/gradebook/__tests__/store.test.ts index 97143ade93..d55b9d6d86 100644 --- a/client/app/bundles/course/gradebook/__tests__/store.test.ts +++ b/client/app/bundles/course/gradebook/__tests__/store.test.ts @@ -151,3 +151,152 @@ describe('UPDATE_TAB_WEIGHTS reducer', () => { ); }); }); + +describe('external assessment reducers', () => { + const state = { + categories: [{ id: 1, title: 'Cat A' }], + tabs: [{ id: 10, title: 'Tab 1', categoryId: 1 }], + assessments: [{ id: 100, title: 'Quiz 1', tabId: 10, maxGrade: 10 }], + students: [], + submissions: [{ studentId: 1, assessmentId: 100, grade: 8 }], + gamificationEnabled: false, + userId: 0, + weightedViewEnabled: false, + canManageWeights: true, + }; + + it('applyCreatedExternal adds category, tab and assessment', () => { + const next = reducer( + state, + actions.applyCreatedExternal({ + assessment: { + id: -5, + title: 'Midterm', + tabId: 200, + maxGrade: 50, + external: true, + }, + tab: { id: 200, title: 'Midterm', categoryId: 2 }, + category: { id: 2, title: 'External Assessments' }, + }), + ); + expect(next.categories.find((c) => c.id === 2)?.title).toBe( + 'External Assessments', + ); + expect(next.tabs.find((t) => t.id === 200)?.title).toBe('Midterm'); + expect(next.assessments.find((a) => a.id === -5)?.external).toBe(true); + }); + + it('applyCreatedExternal does not duplicate an existing category/tab', () => { + const seeded = { + ...state, + categories: [ + ...state.categories, + { id: 2, title: 'External Assessments' }, + ], + tabs: [...state.tabs, { id: 200, title: 'Midterm', categoryId: 2 }], + }; + const next = reducer( + seeded, + actions.applyCreatedExternal({ + assessment: { + id: -6, + title: 'Final', + tabId: 200, + maxGrade: 100, + external: true, + }, + tab: { id: 200, title: 'Midterm', categoryId: 2 }, + category: { id: 2, title: 'External Assessments' }, + }), + ); + expect(next.categories.filter((c) => c.id === 2)).toHaveLength(1); + expect(next.tabs.filter((t) => t.id === 200)).toHaveLength(1); + }); + + it('updateExternalAssessment changes title and maxGrade and syncs tab title', () => { + const seeded = { + ...state, + assessments: [ + ...state.assessments, + { id: -5, title: 'Midterm', tabId: 200, maxGrade: 50, external: true }, + ], + tabs: [...state.tabs, { id: 200, title: 'Midterm', categoryId: 2 }], + }; + const next = reducer( + seeded, + actions.updateExternalAssessment({ + assessment: { + id: -5, + title: 'Midterm Exam', + tabId: 200, + maxGrade: 60, + external: true, + }, + tab: { id: 200, title: 'Midterm Exam', categoryId: 2 }, + }), + ); + expect(next.assessments.find((a) => a.id === -5)?.title).toBe( + 'Midterm Exam', + ); + expect(next.assessments.find((a) => a.id === -5)?.maxGrade).toBe(60); + expect(next.tabs.find((t) => t.id === 200)?.title).toBe('Midterm Exam'); + }); + + it('deleteExternalAssessment removes the assessment and its now-empty tab', () => { + const seeded = { + ...state, + assessments: [ + ...state.assessments, + { id: -5, title: 'Midterm', tabId: 200, maxGrade: 50, external: true }, + ], + tabs: [...state.tabs, { id: 200, title: 'Midterm', categoryId: 2 }], + submissions: [ + ...state.submissions, + { studentId: 1, assessmentId: -5, grade: 30 }, + ], + }; + const next = reducer(seeded, actions.deleteExternalAssessment(-5)); + expect(next.assessments.find((a) => a.id === -5)).toBeUndefined(); + expect(next.tabs.find((t) => t.id === 200)).toBeUndefined(); + expect(next.submissions.find((s) => s.assessmentId === -5)).toBeUndefined(); + }); + + it('setExternalGrade upserts a submission row by (studentId, assessmentId)', () => { + const inserted = reducer( + state, + actions.setExternalGrade({ studentId: 1, assessmentId: -5, grade: 42 }), + ); + expect( + inserted.submissions.find( + (s) => s.studentId === 1 && s.assessmentId === -5, + )?.grade, + ).toBe(42); + + const updated = reducer( + inserted, + actions.setExternalGrade({ studentId: 1, assessmentId: -5, grade: 17 }), + ); + expect( + updated.submissions.filter( + (s) => s.studentId === 1 && s.assessmentId === -5, + ), + ).toHaveLength(1); + expect( + updated.submissions.find( + (s) => s.studentId === 1 && s.assessmentId === -5, + )?.grade, + ).toBe(17); + }); + + it('setExternalGrade can clear a grade to null', () => { + const next = reducer( + state, + actions.setExternalGrade({ studentId: 1, assessmentId: -5, grade: null }), + ); + expect( + next.submissions.find((s) => s.studentId === 1 && s.assessmentId === -5) + ?.grade, + ).toBeNull(); + }); +}); diff --git a/client/app/bundles/course/gradebook/components/AddExternalColumnPrompt.tsx b/client/app/bundles/course/gradebook/components/AddExternalColumnPrompt.tsx new file mode 100644 index 0000000000..12cf4cc110 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/AddExternalColumnPrompt.tsx @@ -0,0 +1,116 @@ +import { FC, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, +} from '@mui/material'; + +import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import { createExternalAssessment } from '../operations'; + +const translations = defineMessages({ + title: { + id: 'course.gradebook.AddExternalColumnPrompt.title', + defaultMessage: 'Add external assessment', + }, + nameLabel: { + id: 'course.gradebook.AddExternalColumnPrompt.nameLabel', + defaultMessage: 'Name', + }, + maxLabel: { + id: 'course.gradebook.AddExternalColumnPrompt.maxLabel', + defaultMessage: 'Max marks', + }, + cancel: { + id: 'course.gradebook.AddExternalColumnPrompt.cancel', + defaultMessage: 'Cancel', + }, + create: { + id: 'course.gradebook.AddExternalColumnPrompt.create', + defaultMessage: 'Create', + }, + error: { + id: 'course.gradebook.AddExternalColumnPrompt.error', + defaultMessage: 'Could not create the external assessment.', + }, + success: { + id: 'course.gradebook.AddExternalColumnPrompt.success', + defaultMessage: 'External assessment created.', + }, +}); + +interface Props { + open: boolean; + onClose: () => void; +} + +const AddExternalColumnPrompt: FC = ({ open, onClose }) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [name, setName] = useState(''); + const [max, setMax] = useState(''); + const [saving, setSaving] = useState(false); + + const reset = (): void => { + setName(''); + setMax(''); + }; + + const canSave = + name.trim() !== '' && max.trim() !== '' && Number(max) >= 0 && !saving; + + const submit = async (): Promise => { + setSaving(true); + try { + await dispatch(createExternalAssessment(name.trim(), Number(max))); + toast.success(t(translations.success)); + reset(); + onClose(); + } catch { + toast.error(t(translations.error)); + } finally { + setSaving(false); + } + }; + + return ( + + {t(translations.title)} + + setName(e.target.value)} + value={name} + /> + setMax(e.target.value)} + type="number" + value={max} + /> + + + + + + + ); +}; + +export default AddExternalColumnPrompt; diff --git a/client/app/bundles/course/gradebook/components/DeleteExternalColumnPrompt.tsx b/client/app/bundles/course/gradebook/components/DeleteExternalColumnPrompt.tsx new file mode 100644 index 0000000000..528fdc6afb --- /dev/null +++ b/client/app/bundles/course/gradebook/components/DeleteExternalColumnPrompt.tsx @@ -0,0 +1,94 @@ +import { FC, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; + +import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import { deleteExternalAssessment } from '../operations'; + +const translations = defineMessages({ + title: { + id: 'course.gradebook.DeleteExternalColumnPrompt.title', + defaultMessage: 'Delete external assessment', + }, + body: { + id: 'course.gradebook.DeleteExternalColumnPrompt.body', + defaultMessage: + 'Delete "{title}"? This permanently removes the column and every student grade in it. This cannot be undone.', + }, + cancel: { + id: 'course.gradebook.DeleteExternalColumnPrompt.cancel', + defaultMessage: 'Cancel', + }, + confirm: { + id: 'course.gradebook.DeleteExternalColumnPrompt.confirm', + defaultMessage: 'Delete', + }, + error: { + id: 'course.gradebook.DeleteExternalColumnPrompt.error', + defaultMessage: 'Could not delete the external assessment.', + }, +}); + +interface Props { + open: boolean; + assessmentId: number; + title: string; + onClose: () => void; +} + +const DeleteExternalColumnPrompt: FC = ({ + open, + assessmentId, + title, + onClose, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [saving, setSaving] = useState(false); + + const submit = async (): Promise => { + setSaving(true); + try { + await dispatch(deleteExternalAssessment(assessmentId)); + onClose(); + } catch { + toast.error(t(translations.error)); + } finally { + setSaving(false); + } + }; + + return ( + + {t(translations.title)} + + {t(translations.body, { title })} + + + + + + + ); +}; + +export default DeleteExternalColumnPrompt; diff --git a/client/app/bundles/course/gradebook/components/GradebookTable.tsx b/client/app/bundles/course/gradebook/components/GradebookTable.tsx index 5c6c7abe4d..3190c6273b 100644 --- a/client/app/bundles/course/gradebook/components/GradebookTable.tsx +++ b/client/app/bundles/course/gradebook/components/GradebookTable.tsx @@ -7,8 +7,13 @@ import { useState, } from 'react'; import { defineMessages } from 'react-intl'; +import { MoreVert } from '@mui/icons-material'; import { Checkbox, + Chip, + IconButton, + Menu, + MenuItem, Paper, type SxProps, Table, @@ -18,6 +23,7 @@ import { TableHead, TableRow, TableSortLabel, + TextField, type Theme, Tooltip, } from '@mui/material'; @@ -36,10 +42,13 @@ import { DEFAULT_TABLE_ROWS_PER_PAGE, } from 'lib/constants/sharedConstants'; import { getEditSubmissionURL } from 'lib/helpers/url-builders'; +import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import tableTranslations from 'lib/translations/table'; import { GAMIFICATION_COL_IDS } from '../constants'; +import { setExternalGrade, updateExternalMaxGrade } from '../operations'; import type { AssessmentData, CategoryData, @@ -52,7 +61,9 @@ import { buildAssessmentColumnId, parseAssessmentColumnId, } from './buildAssessmentColumnIds'; +import DeleteExternalColumnPrompt from './DeleteExternalColumnPrompt'; import GradebookColumnTree from './GradebookColumnTree'; +import RenameExternalColumnPrompt from './RenameExternalColumnPrompt'; const COL_WIDTHS = { name: 160, @@ -110,6 +121,38 @@ const translations = defineMessages({ defaultMessage: 'No grade or gamification columns selected - export will include student info only.', }, + externalBadge: { + id: 'course.gradebook.GradebookTable.externalBadge', + defaultMessage: 'External', + }, + externalGradeAria: { + id: 'course.gradebook.GradebookTable.externalGradeAria', + defaultMessage: '{title} grade for {name}', + }, + gradeSaveError: { + id: 'course.gradebook.GradebookTable.gradeSaveError', + defaultMessage: 'Could not save the grade. Please try again.', + }, + externalMaxAria: { + id: 'course.gradebook.GradebookTable.externalMaxAria', + defaultMessage: '{title} max marks', + }, + maxSaveError: { + id: 'course.gradebook.GradebookTable.maxSaveError', + defaultMessage: 'Could not save the max marks. Please try again.', + }, + rename: { + id: 'course.gradebook.GradebookTable.rename', + defaultMessage: 'Rename', + }, + deleteAction: { + id: 'course.gradebook.GradebookTable.delete', + defaultMessage: 'Delete', + }, + manageAria: { + id: 'course.gradebook.GradebookTable.manageAria', + defaultMessage: 'manage {title}', + }, }); const HeaderLabel = forwardRef< @@ -177,6 +220,144 @@ const HeaderLabel = forwardRef< }); HeaderLabel.displayName = 'HeaderLabel'; +const ExternalGradeCell = ({ + assessmentId, + studentId, + studentName, + title, + value, +}: { + assessmentId: number; + studentId: number; + studentName: string; + title: string; + value: number | null | undefined; +}): JSX.Element => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [editing, setEditing] = useState(false); + const [text, setText] = useState(''); + const [localValue, setLocalValue] = useState( + value, + ); + + const commit = async (): Promise => { + setEditing(false); + const trimmed = text.trim(); + const next = trimmed === '' ? null : Number(trimmed); + if (trimmed !== '' && Number.isNaN(next)) return; + if (next === (localValue ?? null)) return; + const prev = localValue; + setLocalValue(next); + try { + await dispatch(setExternalGrade(assessmentId, studentId, next)); + } catch { + setLocalValue(prev); + toast.error(t(translations.gradeSaveError)); + } + }; + + if (editing) { + return ( + setText(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') commit(); + if (e.key === 'Escape') setEditing(false); + }} + size="small" + value={text} + variant="standard" + /> + ); + } + + return ( + { + setText(localValue == null ? '' : String(localValue)); + setEditing(true); + }} + role="button" + style={{ cursor: 'pointer', display: 'inline-block', minWidth: 24 }} + tabIndex={0} + > + {localValue == null ? '—' : localValue} + + ); +}; + +const ExternalMaxCell = ({ + assessmentId, + title, + value, +}: { + assessmentId: number; + title: string; + value: number; +}): JSX.Element => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [editing, setEditing] = useState(false); + const [text, setText] = useState(''); + + const commit = async (): Promise => { + setEditing(false); + const next = Number(text.trim()); + if (text.trim() === '' || Number.isNaN(next) || next < 0) return; + if (next === value) return; + try { + await dispatch(updateExternalMaxGrade(assessmentId, next)); + } catch { + toast.error(t(translations.maxSaveError)); + } + }; + + if (editing) { + return ( + setText(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') commit(); + if (e.key === 'Escape') setEditing(false); + }} + size="small" + value={text} + variant="standard" + /> + ); + } + + return ( + { + setText(String(value)); + setEditing(true); + }} + role="button" + style={{ cursor: 'pointer' }} + tabIndex={0} + > + {`/${value}`} + + ); +}; + interface GradebookRow { studentId: number; name: string; @@ -211,6 +392,15 @@ const GradebookTable = ({ }: GradebookTableProps): JSX.Element => { const { t } = useTranslation(); + const [menuAnchor, setMenuAnchor] = useState(null); + const [menuAsnId, setMenuAsnId] = useState(null); + const [renameOpen, setRenameOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const menuAsn = + menuAsnId !== null + ? assessments.find((a) => a.id === menuAsnId) + : undefined; + const submissionsByStudent = useMemo(() => { const map = new Map(); submissions.forEach((s) => { @@ -330,6 +520,17 @@ const GradebookTable = ({ }, }, cell: (row) => { + if (asn.external) { + return ( + + ); + } const grade = row.grades[asn.id]; if (grade === undefined) return '—'; if (grade === null) return ''; @@ -598,6 +799,11 @@ const GradebookTable = ({ const isLeft = isLeftAligned(id); const fits = headerFits[id] ?? false; const sort = sortByColId.get(id); + const asnId = parseAssessmentColumnId(id); + const isExternalCol = + asnId !== null && + assessments.find((a) => a.id === asnId)?.external === + true; const labelNode = ( @@ -608,6 +814,17 @@ const GradebookTable = ({ ); + const sortedLabel = sort ? ( + + {labelNode} + + ) : ( + labelNode + ); return ( - {sort ? ( - - {labelNode} - + {sortedLabel} + + { + setMenuAnchor(e.currentTarget); + setMenuAsnId(asnId); + }} + size="small" + > + + + ) : ( - labelNode + sortedLabel )} ); @@ -676,11 +911,23 @@ const GradebookTable = ({ {visibleCols.map((c) => { const id = c.id ?? (c.of as string); const asnId = parseAssessmentColumnId(id); - let cellContent: string = ''; - if (id === 'name') cellContent = t(translations.maxMarks); - else if (asnId !== null) { + const asn = + asnId !== null + ? assessments.find((a) => a.id === asnId) + : undefined; + let cellNode: React.ReactNode = ''; + if (id === 'name') cellNode = t(translations.maxMarks); + else if (asn?.external) { + cellNode = ( + + ); + } else if (asnId !== null) { const maxGrade = assessmentMaxGrades.get(asnId); - cellContent = maxGrade != null ? `/${maxGrade}` : ''; + cellNode = maxGrade != null ? `/${maxGrade}` : ''; } return ( - {cellContent} + {cellNode} ); })} @@ -802,6 +1049,44 @@ const GradebookTable = ({ {pagination && } + setMenuAnchor(null)} + open={Boolean(menuAnchor)} + > + { + setMenuAnchor(null); + setRenameOpen(true); + }} + > + {t(translations.rename)} + + { + setMenuAnchor(null); + setDeleteOpen(true); + }} + > + {t(translations.deleteAction)} + + + {menuAsn && ( + setRenameOpen(false)} + open={renameOpen} + /> + )} + {menuAsn && ( + setDeleteOpen(false)} + open={deleteOpen} + title={menuAsn.title} + /> + )} ); }; diff --git a/client/app/bundles/course/gradebook/components/RenameExternalColumnPrompt.tsx b/client/app/bundles/course/gradebook/components/RenameExternalColumnPrompt.tsx new file mode 100644 index 0000000000..d5876fe2f6 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/RenameExternalColumnPrompt.tsx @@ -0,0 +1,100 @@ +import { FC, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, +} from '@mui/material'; + +import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import { renameExternalAssessment } from '../operations'; + +const translations = defineMessages({ + title: { + id: 'course.gradebook.RenameExternalColumnPrompt.title', + defaultMessage: 'Rename external assessment', + }, + nameLabel: { + id: 'course.gradebook.RenameExternalColumnPrompt.nameLabel', + defaultMessage: 'Name', + }, + cancel: { + id: 'course.gradebook.RenameExternalColumnPrompt.cancel', + defaultMessage: 'Cancel', + }, + save: { + id: 'course.gradebook.RenameExternalColumnPrompt.save', + defaultMessage: 'Save', + }, + error: { + id: 'course.gradebook.RenameExternalColumnPrompt.error', + defaultMessage: 'Could not rename the external assessment.', + }, +}); + +interface Props { + open: boolean; + assessmentId: number; + currentTitle: string; + onClose: () => void; +} + +const RenameExternalColumnPrompt: FC = ({ + open, + assessmentId, + currentTitle, + onClose, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const [name, setName] = useState(currentTitle); + const [saving, setSaving] = useState(false); + + const submit = async (): Promise => { + setSaving(true); + try { + await dispatch(renameExternalAssessment(assessmentId, name.trim())); + onClose(); + } catch { + toast.error(t(translations.error)); + } finally { + setSaving(false); + } + }; + + return ( + + {t(translations.title)} + + setName(e.target.value)} + value={name} + /> + + + + + + + ); +}; + +export default RenameExternalColumnPrompt; diff --git a/client/app/bundles/course/gradebook/components/buildAssessmentColumnIds.ts b/client/app/bundles/course/gradebook/components/buildAssessmentColumnIds.ts index d12a4bd26a..13f84cf48a 100644 --- a/client/app/bundles/course/gradebook/components/buildAssessmentColumnIds.ts +++ b/client/app/bundles/course/gradebook/components/buildAssessmentColumnIds.ts @@ -2,6 +2,6 @@ export const buildAssessmentColumnId = (asnId: number): string => `asn-${asnId}`; export const parseAssessmentColumnId = (colId: string): number | null => { - const match = colId.match(/^asn-(\d+)$/); + const match = colId.match(/^asn-(-?\d+)$/); return match ? Number(match[1]) : null; }; diff --git a/client/app/bundles/course/gradebook/components/import/ExternalGradeConflictPrompt.tsx b/client/app/bundles/course/gradebook/components/import/ExternalGradeConflictPrompt.tsx new file mode 100644 index 0000000000..209f8be72b --- /dev/null +++ b/client/app/bundles/course/gradebook/components/import/ExternalGradeConflictPrompt.tsx @@ -0,0 +1,88 @@ +import { FC } from 'react'; +import { defineMessages } from 'react-intl'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; +import type { ImportConflict } from 'types/course/gradebook'; + +import useTranslation from 'lib/hooks/useTranslation'; + +import ExternalGradeConflictTable from './ExternalGradeConflictTable'; + +const translations = defineMessages({ + title: { + id: 'course.gradebook.ExternalGradeConflictPrompt.title', + defaultMessage: 'Resolve grade conflicts', + }, + body: { + id: 'course.gradebook.ExternalGradeConflictPrompt.body', + defaultMessage: + 'These students already have a grade for these components. Keep their existing grades, or replace them with the values from your file? New students and blank cells are unaffected.', + }, + goBack: { + id: 'course.gradebook.ExternalGradeConflictPrompt.goBack', + defaultMessage: 'Go Back', + }, + keepExisting: { + id: 'course.gradebook.ExternalGradeConflictPrompt.keepExisting', + defaultMessage: 'Keep Existing', + }, + replace: { + id: 'course.gradebook.ExternalGradeConflictPrompt.replace', + defaultMessage: 'Replace', + }, +}); + +interface Props { + open: boolean; + conflicts: ImportConflict[]; + disabled?: boolean; + onKeepExisting: () => void; + onReplaceAll: () => void; + onCancel: () => void; +} + +const ExternalGradeConflictPrompt: FC = ({ + open, + conflicts, + disabled = false, + onKeepExisting, + onReplaceAll, + onCancel, +}) => { + const { t } = useTranslation(); + return ( + + {t(translations.title)} + + {t(translations.body)} + + + + + + + + + + + ); +}; + +export default ExternalGradeConflictPrompt; diff --git a/client/app/bundles/course/gradebook/components/import/ExternalGradeConflictTable.tsx b/client/app/bundles/course/gradebook/components/import/ExternalGradeConflictTable.tsx new file mode 100644 index 0000000000..19972bef83 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/import/ExternalGradeConflictTable.tsx @@ -0,0 +1,86 @@ +import { FC } from 'react'; +import { defineMessages } from 'react-intl'; +import { WarningAmber } from '@mui/icons-material'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, + Typography, +} from '@mui/material'; +import type { ImportConflict } from 'types/course/gradebook'; + +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + component: { + id: 'course.gradebook.ExternalGradeConflictTable.component', + defaultMessage: 'Component', + }, + student: { + id: 'course.gradebook.ExternalGradeConflictTable.student', + defaultMessage: 'Student', + }, + existing: { + id: 'course.gradebook.ExternalGradeConflictTable.existing', + defaultMessage: 'Existing grade', + }, + inFile: { + id: 'course.gradebook.ExternalGradeConflictTable.inFile', + defaultMessage: 'In-file grade', + }, + mismatch: { + id: 'course.gradebook.ExternalGradeConflictTable.mismatch', + defaultMessage: + 'This identifier now resolves to a different student than the existing grade was imported under.', + }, +}); + +interface Props { + rows: ImportConflict[]; +} + +const ExternalGradeConflictTable: FC = ({ rows }) => { + const { t } = useTranslation(); + return ( + + + + {t(translations.component)} + {t(translations.student)} + {t(translations.existing)} + {t(translations.inFile)} + + + + {rows.map((row) => ( + + {row.component} + + {row.studentName} + {row.identifierMismatch && ( + + + + )} + + {row.existingGrade} + + + {row.inFileGrade} + + + + ))} + +
+ ); +}; + +export default ExternalGradeConflictTable; diff --git a/client/app/bundles/course/gradebook/components/import/ImportExternalAssessmentsButton.tsx b/client/app/bundles/course/gradebook/components/import/ImportExternalAssessmentsButton.tsx new file mode 100644 index 0000000000..279b239603 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/import/ImportExternalAssessmentsButton.tsx @@ -0,0 +1,44 @@ +import { FC, useMemo, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { Button } from '@mui/material'; + +import { useAppSelector } from 'lib/hooks/store'; +import useTranslation from 'lib/hooks/useTranslation'; + +import { getAssessments, getWeightedViewEnabled } from '../../selectors'; + +import ImportExternalAssessmentsWizard from './ImportExternalAssessmentsWizard'; + +const translations = defineMessages({ + importButton: { + id: 'course.gradebook.ImportExternalAssessmentsButton.label', + defaultMessage: 'Import external assessments', + }, +}); + +const ImportExternalAssessmentsButton: FC = () => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const weightedViewEnabled = useAppSelector(getWeightedViewEnabled); + const assessments = useAppSelector(getAssessments); + const existingExternalTitles = useMemo( + () => assessments.filter((a) => a.external).map((a) => a.title), + [assessments], + ); + + return ( + <> + + setOpen(false)} + open={open} + weightedViewEnabled={weightedViewEnabled} + /> + + ); +}; + +export default ImportExternalAssessmentsButton; diff --git a/client/app/bundles/course/gradebook/components/import/ImportExternalAssessmentsWizard.tsx b/client/app/bundles/course/gradebook/components/import/ImportExternalAssessmentsWizard.tsx new file mode 100644 index 0000000000..032caf39d1 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/import/ImportExternalAssessmentsWizard.tsx @@ -0,0 +1,460 @@ +import { FC, useEffect, useMemo, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { useParams } from 'react-router-dom'; +import { Add, Delete } from '@mui/icons-material'; +import { + Alert, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Link as MuiLink, + Step, + StepLabel, + Stepper, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + TextField, + ToggleButton, + ToggleButtonGroup, +} from '@mui/material'; +import type { + IdentifierMode, + ImportComponent, + ImportPreviewResult, +} from 'types/course/gradebook'; + +import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import { commitImport, previewImport } from '../../operations'; + +import { downloadTemplate, readFileText } from './buildTemplate'; +import ExternalGradeConflictPrompt from './ExternalGradeConflictPrompt'; + +const translations = defineMessages({ + title: { + id: 'course.gradebook.ImportWizard.title', + defaultMessage: 'Import external assessments', + }, + stepDefine: { + id: 'course.gradebook.ImportWizard.stepDefine', + defaultMessage: 'Define components', + }, + stepUpload: { + id: 'course.gradebook.ImportWizard.stepUpload', + defaultMessage: 'Template & upload', + }, + stepVerify: { + id: 'course.gradebook.ImportWizard.stepVerify', + defaultMessage: 'Verify', + }, + componentName: { + id: 'course.gradebook.ImportWizard.componentName', + defaultMessage: 'Component name', + }, + weightage: { + id: 'course.gradebook.ImportWizard.weightage', + defaultMessage: 'Weightage', + }, + maxMarks: { + id: 'course.gradebook.ImportWizard.maxMarks', + defaultMessage: 'Max marks', + }, + addComponent: { + id: 'course.gradebook.ImportWizard.addComponent', + defaultMessage: 'Add component', + }, + updatesExisting: { + id: 'course.gradebook.ImportWizard.updatesExisting', + defaultMessage: 'Updates existing — managed in the gradebook', + }, + identifierMode: { + id: 'course.gradebook.ImportWizard.identifierMode', + defaultMessage: 'Match students by', + }, + studentId: { + id: 'course.gradebook.ImportWizard.studentId', + defaultMessage: 'Student ID', + }, + email: { id: 'course.gradebook.ImportWizard.email', defaultMessage: 'Email' }, + studentIdHint: { + id: 'course.gradebook.ImportWizard.studentIdHint', + defaultMessage: + "Matching uses each student's current Student ID. Keep Student IDs up to date in Manage Users.", + }, + downloadTemplate: { + id: 'course.gradebook.ImportWizard.downloadTemplate', + defaultMessage: 'Download template', + }, + upload: { + id: 'course.gradebook.ImportWizard.upload', + defaultMessage: 'Upload filled CSV', + }, + back: { id: 'course.gradebook.ImportWizard.back', defaultMessage: 'Back' }, + next: { id: 'course.gradebook.ImportWizard.next', defaultMessage: 'Next' }, + verify: { + id: 'course.gradebook.ImportWizard.verify', + defaultMessage: 'Verify', + }, + cancel: { + id: 'course.gradebook.ImportWizard.cancel', + defaultMessage: 'Cancel', + }, + continue: { + id: 'course.gradebook.ImportWizard.continue', + defaultMessage: 'Confirm import', + }, + unresolved: { + id: 'course.gradebook.ImportWizard.unresolved', + defaultMessage: 'These identifiers were not found in the course: {ids}', + }, + malformed: { + id: 'course.gradebook.ImportWizard.malformed', + defaultMessage: 'These cells are not valid numbers: {cells}', + }, + committed: { + id: 'course.gradebook.ImportWizard.committed', + defaultMessage: 'Import complete.', + }, + commitError: { + id: 'course.gradebook.ImportWizard.commitError', + defaultMessage: 'Import failed. Nothing was saved.', + }, + previewError: { + id: 'course.gradebook.ImportWizard.previewError', + defaultMessage: 'Could not verify the file. Please try again.', + }, +}); + +interface Props { + open: boolean; + onClose: () => void; + weightedViewEnabled: boolean; + existingExternalTitles: string[]; +} + +let rowId = 0; +const blankComponent = (): ImportComponent & { id: number } => { + rowId += 1; + return { id: rowId, name: '', weightage: 0, maximumGrade: 0 }; +}; + +const ImportExternalAssessmentsWizard: FC = ({ + open, + onClose, + weightedViewEnabled, + existingExternalTitles, +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { courseId } = useParams(); + const [step, setStep] = useState(0); + const [components, setComponents] = useState< + (ImportComponent & { id: number })[] + >([blankComponent()]); + const [mode, setMode] = useState('student_id'); + const [csvData, setCsvData] = useState(''); + const [preview, setPreview] = useState(null); + const [conflictOpen, setConflictOpen] = useState(false); + const [busy, setBusy] = useState(false); + + useEffect(() => { + if (!open) { + setStep(0); + setComponents([blankComponent()]); + setCsvData(''); + setPreview(null); + setConflictOpen(false); + setBusy(false); + } + }, [open]); + + const existingSet = useMemo( + () => new Set(existingExternalTitles), + [existingExternalTitles], + ); + const isExisting = (name: string): boolean => existingSet.has(name.trim()); + + const updateComponent = ( + i: number, + patch: Partial, + ): void => + setComponents((cs) => cs.map((c, j) => (j === i ? { ...c, ...patch } : c))); + + const defineValid = + components.length > 0 && + components.every((c) => c.name.trim() !== '') && + new Set(components.map((c) => c.name.trim())).size === components.length; + + const runPreview = async (): Promise => { + setBusy(true); + try { + const result = await dispatch( + previewImport({ + components: components.map(({ id: _, ...rest }) => rest), + identifierMode: mode, + csvData, + }), + ); + setPreview(result); + setStep(2); + } catch { + toast.error(t(translations.previewError)); + } finally { + setBusy(false); + } + }; + + const doCommit = async (onConflict: 'keep' | 'replace'): Promise => { + setBusy(true); + try { + await dispatch( + commitImport({ + components: components.map(({ id: _, ...rest }) => rest), + identifierMode: mode, + csvData, + onConflict, + }), + ); + toast.success(t(translations.committed)); + setConflictOpen(false); + onClose(); + } catch { + toast.error(t(translations.commitError)); + } finally { + setBusy(false); + } + }; + + const onConfirm = (): void => { + if (preview && preview.conflicts.length > 0) setConflictOpen(true); + else doCommit('replace'); + }; + + return ( + + {t(translations.title)} + + + + {t(translations.stepDefine)} + + + {t(translations.stepUpload)} + + + {t(translations.stepVerify)} + + + + {step === 0 && ( + <> + {components.map((c, i) => { + const locked = isExisting(c.name); + return ( +
+ + updateComponent(i, { name: e.target.value }) + } + size="small" + value={c.name} + /> + {weightedViewEnabled && ( + + updateComponent(i, { + weightage: Number(e.target.value), + }) + } + size="small" + type="number" + value={c.weightage} + /> + )} + + updateComponent(i, { + maximumGrade: Number(e.target.value), + }) + } + size="small" + type="number" + value={c.maximumGrade} + /> + {locked && ( + + {t(translations.updatesExisting)} + + )} + + setComponents((cs) => cs.filter((_, j) => j !== i)) + } + size="small" + > + + +
+ ); + })} + + +
+ {t(translations.identifierMode)} + v && setMode(v)} + size="small" + value={mode} + > + + {t(translations.studentId)} + + + {t(translations.email)} + + +
+ {mode === 'student_id' && ( + + {t(translations.studentIdHint)}{' '} + + Manage Users + + + )} + + )} + + {step === 1 && ( +
+ + { + const f = (e.target as HTMLInputElement).files?.[0]; + if (f) setCsvData(await readFileText(f)); + }} + type="file" + /> +
+ )} + + {step === 2 && preview && ( + <> + {!preview.ok && preview.unresolved.length > 0 && ( + + {t(translations.unresolved, { + ids: preview.unresolved.join(', '), + })} + + )} + {!preview.ok && preview.malformed.length > 0 && ( + + {t(translations.malformed, { + cells: preview.malformed.join('; '), + })} + + )} + {preview.ok && ( + + + + {t(translations.componentName)} + {components.map((c) => ( + {c.name} + ))} + + + + {preview.sample.map((row) => ( + + {row.studentName} + {components.map((c) => ( + + {row.grades[c.name] ?? '—'} + + ))} + + ))} + +
+ )} + + )} +
+ + + {step > 0 && ( + + )} + {step === 0 && ( + + )} + {step === 1 && ( + + )} + {step === 2 && preview?.ok && ( + + )} + + + setConflictOpen(false)} + onKeepExisting={() => doCommit('keep')} + onReplaceAll={() => doCommit('replace')} + open={conflictOpen} + /> +
+ ); +}; + +export default ImportExternalAssessmentsWizard; diff --git a/client/app/bundles/course/gradebook/components/import/buildTemplate.ts b/client/app/bundles/course/gradebook/components/import/buildTemplate.ts new file mode 100644 index 0000000000..14da3e2d42 --- /dev/null +++ b/client/app/bundles/course/gradebook/components/import/buildTemplate.ts @@ -0,0 +1,34 @@ +import type { ImportComponent } from 'types/course/gradebook'; + +const csvCell = (value: string): string => + /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value; + +// Header-only template: Identifier + one column per component (dialog order). +export const buildTemplateCsv = (components: ImportComponent[]): string => { + const header = ['Identifier', ...components.map((c) => c.name)] + .map(csvCell) + .join(','); + return `${header}\n`; +}; + +// Triggers a client-side download of the template. +export const downloadTemplate = (components: ImportComponent[]): void => { + const blob = new Blob([buildTemplateCsv(components)], { + type: 'text/csv;charset=utf-8;', + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'external_assessments_template.csv'; + link.click(); + URL.revokeObjectURL(url); +}; + +// Reads an uploaded File to text (raw CSV; the server parses authoritatively). +export const readFileText = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (): void => resolve(String(reader.result)); + reader.onerror = (): void => reject(reader.error); + reader.readAsText(file); + }); diff --git a/client/app/bundles/course/gradebook/operations.ts b/client/app/bundles/course/gradebook/operations.ts index ae2f962fbb..99fbec280b 100644 --- a/client/app/bundles/course/gradebook/operations.ts +++ b/client/app/bundles/course/gradebook/operations.ts @@ -1,5 +1,10 @@ import type { Operation } from 'store'; -import type { UpdateWeightsPayload } from 'types/course/gradebook'; +import type { + ImportCommitSummary, + ImportPreviewRequest, + ImportPreviewResult, + UpdateWeightsPayload, +} from 'types/course/gradebook'; import CourseAPI from 'api/course'; @@ -17,4 +22,84 @@ export const updateGradebookWeights = dispatch(actions.updateTabWeights(response.data)); }; +export const createExternalAssessment = + (title: string, maximumGrade: number): Operation => + async (dispatch) => { + const response = await CourseAPI.gradebook.createExternal({ + title, + maximumGrade, + }); + dispatch(actions.applyCreatedExternal(response.data)); + }; + +export const renameExternalAssessment = + (assessmentId: number, title: string): Operation => + async (dispatch) => { + const response = await CourseAPI.gradebook.updateExternal(-assessmentId, { + title, + }); + dispatch(actions.updateExternalAssessment(response.data)); + }; + +export const updateExternalMaxGrade = + (assessmentId: number, maximumGrade: number): Operation => + async (dispatch) => { + const response = await CourseAPI.gradebook.updateExternal(-assessmentId, { + maximumGrade, + }); + dispatch(actions.updateExternalAssessment(response.data)); + }; + +export const deleteExternalAssessment = + (assessmentId: number): Operation => + async (dispatch) => { + await CourseAPI.gradebook.deleteExternal(-assessmentId); + dispatch(actions.deleteExternalAssessment(assessmentId)); + }; + +// Optimistic: apply the new grade immediately, then reconcile with the server. +// On failure, restore the previous value and rethrow so the caller can toast. +export const setExternalGrade = + (assessmentId: number, studentId: number, grade: number | null): Operation => + async (dispatch, getState) => { + const prev = + getState().gradebook.submissions.find( + (s) => s.studentId === studentId && s.assessmentId === assessmentId, + )?.grade ?? null; + dispatch(actions.setExternalGrade({ studentId, assessmentId, grade })); + try { + const response = await CourseAPI.gradebook.setExternalGrade( + -assessmentId, + { + studentId, + grade, + }, + ); + dispatch(actions.setExternalGrade(response.data)); + } catch (error) { + dispatch( + actions.setExternalGrade({ studentId, assessmentId, grade: prev }), + ); + throw error; + } + }; + +export const previewImport = + (payload: ImportPreviewRequest): Operation => + async () => { + const response = await CourseAPI.gradebook.importPreview(payload); + return response.data; + }; + +export const commitImport = + ( + payload: ImportPreviewRequest & { onConflict: 'keep' | 'replace' }, + ): Operation => + async (dispatch) => { + const response = await CourseAPI.gradebook.importCommit(payload); + const refreshed = await CourseAPI.gradebook.index(); + dispatch(actions.saveGradebook(refreshed.data)); + return response.data; + }; + export default fetchGradebook; diff --git a/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx b/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx index ae7fc55e02..c2276c6ba1 100644 --- a/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx +++ b/client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx @@ -2,7 +2,7 @@ import { FC, useEffect, useState, useTransition } from 'react'; import { defineMessages } from 'react-intl'; import { useParams, useSearchParams } from 'react-router-dom'; import { PeopleAlt } from '@mui/icons-material'; -import { Tab, Tabs, Typography } from '@mui/material'; +import { Button, Tab, Tabs, Typography } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; @@ -11,9 +11,11 @@ import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { useCourseContext } from '../../../container/CourseLoader'; +import AddExternalColumnPrompt from '../../components/AddExternalColumnPrompt'; import GradebookTable from '../../components/GradebookTable'; import GradebookWeightedTable from '../../components/GradebookWeightedTable'; import GradeLinkHint from '../../components/GradeLinkHint'; +import ImportExternalAssessmentsButton from '../../components/import/ImportExternalAssessmentsButton'; import WeightedViewHint from '../../components/WeightedViewHint'; import fetchGradebook from '../../operations'; import { @@ -52,6 +54,10 @@ const translations = defineMessages({ id: 'course.gradebook.GradebookIndex.byWeight', defaultMessage: 'Weighted total', }, + addExternal: { + id: 'course.gradebook.GradebookIndex.addExternal', + defaultMessage: 'Add external assessment', + }, }); const GradebookIndex: FC = () => { @@ -66,6 +72,7 @@ const GradebookIndex: FC = () => { searchParams.get('view') === 'weighted' ? 'weighted' : 'all', ); const [isPending, startTransition] = useTransition(); + const [addOpen, setAddOpen] = useState(false); const assessments = useAppSelector(getAssessments); const categories = useAppSelector(getCategories); @@ -149,6 +156,22 @@ const GradebookIndex: FC = () => { {!isLoading && students.length > 0 && !(weightedViewEnabled && viewMode === 'weighted') && } + {!isLoading && canManageWeights && students.length > 0 && ( +
+ + +
+ )} + setAddOpen(false)} + open={addOpen} + />
{isPending && (
diff --git a/client/app/bundles/course/gradebook/store.ts b/client/app/bundles/course/gradebook/store.ts index 4777e16b3b..94a7e0041d 100644 --- a/client/app/bundles/course/gradebook/store.ts +++ b/client/app/bundles/course/gradebook/store.ts @@ -1,5 +1,8 @@ import { produce } from 'immer'; import type { + ExternalAssessmentNode, + ExternalAssessmentUpdate, + ExternalGradePayload, GradebookData, UpdateWeightsPayload, } from 'types/course/gradebook'; @@ -14,6 +17,12 @@ import type { const SAVE_GRADEBOOK = 'course/gradebook/SAVE_GRADEBOOK'; const UPDATE_TAB_WEIGHTS = 'course/gradebook/UPDATE_TAB_WEIGHTS'; +const APPLY_CREATED_EXTERNAL = 'course/gradebook/APPLY_CREATED_EXTERNAL'; +const UPDATE_EXTERNAL_ASSESSMENT = + 'course/gradebook/UPDATE_EXTERNAL_ASSESSMENT'; +const DELETE_EXTERNAL_ASSESSMENT = + 'course/gradebook/DELETE_EXTERNAL_ASSESSMENT'; +const SET_EXTERNAL_GRADE = 'course/gradebook/SET_EXTERNAL_GRADE'; interface GradebookState { categories: CategoryData[]; @@ -36,6 +45,23 @@ interface UpdateTabWeightsAction { payload: UpdateWeightsPayload; } +interface ApplyCreatedExternalAction { + type: typeof APPLY_CREATED_EXTERNAL; + payload: ExternalAssessmentNode; +} +interface UpdateExternalAssessmentAction { + type: typeof UPDATE_EXTERNAL_ASSESSMENT; + payload: ExternalAssessmentUpdate; +} +interface DeleteExternalAssessmentAction { + type: typeof DELETE_EXTERNAL_ASSESSMENT; + payload: number; // negative serialized assessment id +} +interface SetExternalGradeAction { + type: typeof SET_EXTERNAL_GRADE; + payload: ExternalGradePayload; +} + const initialState: GradebookState = { categories: [], tabs: [], @@ -50,7 +76,13 @@ const initialState: GradebookState = { const reducer = produce( ( draft: GradebookState, - action: SaveGradebookAction | UpdateTabWeightsAction, + action: + | SaveGradebookAction + | UpdateTabWeightsAction + | ApplyCreatedExternalAction + | UpdateExternalAssessmentAction + | DeleteExternalAssessmentAction + | SetExternalGradeAction, ) => { switch (action.type) { case SAVE_GRADEBOOK: { @@ -99,6 +131,54 @@ const reducer = produce( ); break; } + case APPLY_CREATED_EXTERNAL: { + const { assessment, tab, category } = action.payload; + if (!draft.categories.some((c) => c.id === category.id)) { + draft.categories.push(category); + } + if (!draft.tabs.some((t) => t.id === tab.id)) { + draft.tabs.push(tab); + } + if (!draft.assessments.some((a) => a.id === assessment.id)) { + draft.assessments.push(assessment); + } + break; + } + case UPDATE_EXTERNAL_ASSESSMENT: { + const { assessment, tab } = action.payload; + const a = draft.assessments.find((x) => x.id === assessment.id); + if (a) { + a.title = assessment.title; + a.maxGrade = assessment.maxGrade; + } + const t = draft.tabs.find((x) => x.id === tab.id); + if (t) t.title = tab.title; + break; + } + case DELETE_EXTERNAL_ASSESSMENT: { + const id = action.payload; + const removed = draft.assessments.find((a) => a.id === id); + draft.assessments = draft.assessments.filter((a) => a.id !== id); + draft.submissions = draft.submissions.filter( + (s) => s.assessmentId !== id, + ); + if ( + removed && + !draft.assessments.some((a) => a.tabId === removed.tabId) + ) { + draft.tabs = draft.tabs.filter((t) => t.id !== removed.tabId); + } + break; + } + case SET_EXTERNAL_GRADE: { + const { studentId, assessmentId, grade } = action.payload; + const existing = draft.submissions.find( + (s) => s.studentId === studentId && s.assessmentId === assessmentId, + ); + if (existing) existing.grade = grade; + else draft.submissions.push({ studentId, assessmentId, grade }); + break; + } default: break; } @@ -117,6 +197,22 @@ export const actions = { type: UPDATE_TAB_WEIGHTS, payload, }), + applyCreatedExternal: ( + payload: ExternalAssessmentNode, + ): ApplyCreatedExternalAction => ({ type: APPLY_CREATED_EXTERNAL, payload }), + updateExternalAssessment: ( + payload: ExternalAssessmentUpdate, + ): UpdateExternalAssessmentAction => ({ + type: UPDATE_EXTERNAL_ASSESSMENT, + payload, + }), + deleteExternalAssessment: (id: number): DeleteExternalAssessmentAction => ({ + type: DELETE_EXTERNAL_ASSESSMENT, + payload: id, + }), + setExternalGrade: ( + payload: ExternalGradePayload, + ): SetExternalGradeAction => ({ type: SET_EXTERNAL_GRADE, payload }), }; export default reducer; diff --git a/client/app/types/course/gradebook.ts b/client/app/types/course/gradebook.ts index c9009afbfe..7cc0facc9b 100644 --- a/client/app/types/course/gradebook.ts +++ b/client/app/types/course/gradebook.ts @@ -18,6 +18,7 @@ export interface AssessmentData { maxGrade: number; gradebookWeight?: number | null; gradebookExcluded?: boolean; + external?: boolean; } export interface StudentData { @@ -32,7 +33,7 @@ export interface StudentData { export interface SubmissionData { studentId: number; assessmentId: number; - submissionId: number; + submissionId?: number; grade: number | null; } @@ -56,3 +57,56 @@ export interface UpdateWeightsPayload { assessmentWeights?: { assessmentId: number; weight: number }[]; }[]; } + +export interface ExternalAssessmentNode { + assessment: AssessmentData; + tab: TabData; + category: CategoryData; +} + +export interface ExternalAssessmentUpdate { + assessment: AssessmentData; + tab: Pick; +} + +export interface ExternalGradePayload { + studentId: number; + assessmentId: number; + grade: number | null; +} + +export type IdentifierMode = 'email' | 'student_id'; + +export interface ImportComponent { + name: string; + weightage: number; + maximumGrade: number; +} + +export interface ImportPreviewRequest { + components: ImportComponent[]; + identifierMode: IdentifierMode; + csvData: string; +} + +export interface ImportConflict { + component: string; + studentName: string; + existingGrade: number; + inFileGrade: number; + identifierMismatch: boolean; +} + +export interface ImportPreviewResult { + ok: boolean; + unresolved: string[]; + malformed: string[]; + sample: { studentName: string; grades: Record }[]; + conflicts: ImportConflict[]; +} + +export interface ImportCommitSummary { + createdComponents: number; + updatedComponents: number; + gradesWritten: number; +} diff --git a/client/locales/en.json b/client/locales/en.json index 1d9bf1cce6..a6e87edcdc 100644 --- a/client/locales/en.json +++ b/client/locales/en.json @@ -1,4 +1,7 @@ { + "2kgbTg": { + "defaultMessage": "Role-Playing Assessment" + }, "announcements.GlobalAnnouncementIndex.fetchAnnouncementsFailure": { "defaultMessage": "Unable to fetch announcements" }, @@ -29,6 +32,9 @@ "app.DashboardPage.yourCourses": { "defaultMessage": "Your Courses" }, + "app.ErrorPage.courseSuspended": { + "defaultMessage": "This course is suspended." + }, "app.ErrorPage.error": { "defaultMessage": "KABOOM, a meteor has just crashed." }, @@ -59,14 +65,11 @@ "app.ErrorPage.notFoundSubtitle": { "defaultMessage": "Check if you've typed the correct address, try again later, or go back home." }, - "app.ErrorPage.userSuspended": { - "defaultMessage": "Your access to this course has been suspended." - }, "app.ErrorPage.suspendedSubtitle": { "defaultMessage": "Please contact your instructors or the course staff." }, - "app.ErrorPage.courseSuspended": { - "defaultMessage": "This course is suspended." + "app.ErrorPage.userSuspended": { + "defaultMessage": "Your access to this course has been suspended." }, "app.Footer.contactUs": { "defaultMessage": "Contact Us" @@ -80,12 +83,12 @@ "app.Footer.instructorsGuide": { "defaultMessage": "Instructors' Guide" }, - "app.Footer.reportIssue": { - "defaultMessage": "Report an Issue" - }, "app.Footer.privacyPolicy": { "defaultMessage": "Privacy Policy" }, + "app.Footer.reportIssue": { + "defaultMessage": "Report an Issue" + }, "app.Footer.termsOfService": { "defaultMessage": "Terms of Service" }, @@ -101,6 +104,9 @@ "assessment.attemptLoader.errorAttemptingAssessment": { "defaultMessage": "An error occurred while attempting this assessment. Try again later." }, + "bW7B87": { + "defaultMessage": "New" + }, "client.video.attemptLoader.errorWatchVideo": { "defaultMessage": "An error occurred while attempting to watch this video. Try again later." }, @@ -263,23 +269,23 @@ "course.achievement.AchievementAward.AchievementAwardManager.saveChanges": { "defaultMessage": "Save Changes" }, + "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { + "defaultMessage": "Awarded Students" + }, "course.achievement.AchievementAward.AchievementAwardSummary.name": { "defaultMessage": "Name" }, - "course.achievement.AchievementAward.AchievementAwardSummary.userType": { - "defaultMessage": "User Type" + "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { + "defaultMessage": "Normal Student" }, - "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { - "defaultMessage": "Awarded Students" + "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { + "defaultMessage": "Phantom Student" }, "course.achievement.AchievementAward.AchievementAwardSummary.revokedStudents": { "defaultMessage": "Revoked Students" }, - "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { - "defaultMessage": "Phantom Student" - }, - "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { - "defaultMessage": "Normal Student" + "course.achievement.AchievementAward.AchievementAwardSummary.userType": { + "defaultMessage": "User Type" }, "course.achievement.AchievementAward.awardAchievement": { "defaultMessage": "Award Achievement" @@ -353,26 +359,26 @@ "course.achievement.AchievementShow.studentsWithAchievement": { "defaultMessage": "Students with this achievement" }, - "course.achievement.AchievementTable.noAchievement": { - "defaultMessage": "No achievement" + "course.achievement.AchievementTable.actions": { + "defaultMessage": "Actions" }, "course.achievement.AchievementTable.badge": { "defaultMessage": "Badge" }, - "course.achievement.AchievementTable.title": { - "defaultMessage": "Title" - }, "course.achievement.AchievementTable.description": { "defaultMessage": "Description" }, - "course.achievement.AchievementTable.requirements": { - "defaultMessage": "Requirements" + "course.achievement.AchievementTable.noAchievement": { + "defaultMessage": "No achievement" }, "course.achievement.AchievementTable.published": { "defaultMessage": "Published" }, - "course.achievement.AchievementTable.actions": { - "defaultMessage": "Actions" + "course.achievement.AchievementTable.requirements": { + "defaultMessage": "Requirements" + }, + "course.achievement.AchievementTable.title": { + "defaultMessage": "Title" }, "course.achievement.AchievementsIndex.achievements": { "defaultMessage": "Achievements" @@ -515,45 +521,6 @@ "course.admin.AssessmentSettings.toTab": { "defaultMessage": "to {tab}" }, - "course.admin.CodaveriSettings.codaveriModel": { - "defaultMessage": "Model" - }, - "course.admin.CodaveriSettings.codaveriModelDescription": { - "defaultMessage": "The AI model used by Codaveri to generate help conversations with students for programming questions." - }, - "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { - "defaultMessage": "You may customize the behavior of the Codaveri model by providing instructions here. {br} When assisting students, these instructions will be followed in addition to any you have set on the question itself.{br}To reference question-specific details, you may use the following variables within the prompt, writing them with brackets as shown below:" - }, - "course.admin.CodaveriSettings.codaveriSystemPromptProblemDescriptionLine": { - "defaultMessage": "{problemDescriptionVar} : The full description of the coding problem." - }, - "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { - "defaultMessage": "{studentFilePathsVar} : A comma-separated list of file paths the student is working on." - }, - "course.admin.CodaveriSettings.codaveriSettings": { - "defaultMessage": "Codaveri settings" - }, - "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { - "defaultMessage": "This is currently an experimental feature. Codaveri provides code evaluation and automated code feedback services for students' codes." - }, - "course.admin.CodaveriSettings.feedbackWorkflow": { - "defaultMessage": "Automatic Post-Submission Comments" - }, - "course.admin.CodaveriSettings.feedbackWorkflowDescription": { - "defaultMessage": "When a submission with programming question is finalised," - }, - "course.admin.CodaveriSettings.feedbackWorkflowNone": { - "defaultMessage": "Generate no feedback" - }, - "course.admin.CodaveriSettings.feedbackWorkflowDraft": { - "defaultMessage": "Generate feedback as a draft requiring approval from staff" - }, - "course.admin.CodaveriSettings.feedbackWorkflowPublish": { - "defaultMessage": "Publish feedback directly to student" - }, - "course.admin.CodaveriSettings.error": { - "defaultMessage": "An error occurred while updating the codaveri setting." - }, "course.admin.CodaveriSettings.Some": { "defaultMessage": "Some" }, @@ -569,32 +536,80 @@ "course.admin.CodaveriSettings.codaveriEngineDescription": { "defaultMessage": "Type of codaveri engine used to generate programming code feedback" }, + "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { + "defaultMessage": "Codaveri Evaluator" + }, + "course.admin.CodaveriSettings.codaveriModel": { + "defaultMessage": "Model" + }, + "course.admin.CodaveriSettings.codaveriModelDescription": { + "defaultMessage": "The AI model used by Codaveri to generate help conversations with students for programming questions." + }, "course.admin.CodaveriSettings.codaveriOverrideSystemPrompt": { "defaultMessage": "Use a custom system prompt" }, "course.admin.CodaveriSettings.codaveriOverrideSystemPromptDescription": { "defaultMessage": "When assisting students, these instructions will be followed in addition to any you have set on the question itself. To reference question-specific details, you may use these variables within the prompt, writing them with brackets as shown below:" }, + "course.admin.CodaveriSettings.codaveriSettings": { + "defaultMessage": "Codaveri settings" + }, + "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { + "defaultMessage": "This is currently an experimental feature. Codaveri provides code evaluation and automated code feedback services for students' codes." + }, "course.admin.CodaveriSettings.codaveriSystemPrompt": { "defaultMessage": "System Prompt" }, + "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { + "defaultMessage": "The Codaveri system prompt controls AI behavior when interacting with students." + }, + "course.admin.CodaveriSettings.codaveriSystemPromptProblemDescriptionLine": { + "defaultMessage": "{problemDescriptionVar} : The full description of the coding problem." + }, + "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { + "defaultMessage": "{studentFilePathsVar} : A comma-separated list of file paths the student is working on." + }, "course.admin.CodaveriSettings.codaveriUseDefaultSystemPrompt": { "defaultMessage": "Use the default system prompt" }, + "course.admin.CodaveriSettings.enableDisableButton": { + "defaultMessage": "{enabled, select, true {Enable} other {Disable}}" + }, + "course.admin.CodaveriSettings.enableDisableEvaluator": { + "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Codaveri Evaluator for {questionCount} programming questions in {title}?" + }, + "course.admin.CodaveriSettings.enableDisableEvaluatorDescription": { + "defaultMessage": "{questionCount} programming questions in this {type} will use {enabled, select, true {Codaveri } other {Default }} evaluator" + }, + "course.admin.CodaveriSettings.enableDisableLiveFeedback": { + "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Get Help for {questionCount} programming questions in {title}?" + }, + "course.admin.CodaveriSettings.errorOccurredWhenUpdatingCodaveriEvaluatorSettings": { + "defaultMessage": "An error occurred while updating the codaveri evaluator settings." + }, + "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { + "defaultMessage": "An error occurred while updating the Get Help settings." + }, "course.admin.CodaveriSettings.evaluatorUpdateSuccess": { "defaultMessage": "{question} is now using {evaluator} evaluator" }, "course.admin.CodaveriSettings.expandAll": { "defaultMessage": "Expand All Questions" }, - "course.admin.CodaveriSettings.programmingQuestionSettings": { - "defaultMessage": "Programming Question Settings" + "course.admin.CodaveriSettings.feedbackWorkflow": { + "defaultMessage": "Automatic Post-Submission Comments" }, - "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { - "defaultMessage": "Enable/disable Codaveri as evaluator for programming questions in various assessments." + "course.admin.CodaveriSettings.feedbackWorkflowDescription": { + "defaultMessage": "When a submission with programming question is finalised," }, - "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { - "defaultMessage": "Successfully updated all questions to use {evaluator} evaluator" + "course.admin.CodaveriSettings.feedbackWorkflowDraft": { + "defaultMessage": "Generate feedback as a draft requiring approval from staff" + }, + "course.admin.CodaveriSettings.feedbackWorkflowNone": { + "defaultMessage": "Generate no feedback" + }, + "course.admin.CodaveriSettings.feedbackWorkflowPublish": { + "defaultMessage": "Publish feedback directly to student" }, "course.admin.CodaveriSettings.getHelpUsageLimit": { "defaultMessage": "Limit Get Help messages per student" @@ -602,35 +617,23 @@ "course.admin.CodaveriSettings.getHelpUsageLimitDescription": { "defaultMessage": "If enabled, students will only be able to send a limited number of messages per question. Students will be able to see this limit and how many messages they have left." }, - "course.admin.CodaveriSettings.maxGetHelpUserMessages": { - "defaultMessage": "Maximum messages per question" - }, - "course.admin.CodaveriSettings.errorOccurredWhenUpdatingCodaveriEvaluatorSettings": { - "defaultMessage": "An error occurred while updating the codaveri evaluator settings." - }, - "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { - "defaultMessage": "Codaveri Evaluator" + "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { + "defaultMessage": "Get Help for {question} is now {liveFeedbackEnabled, select, true {enabled} other {disabled}}" }, "course.admin.CodaveriSettings.liveFeedbackSettings": { "defaultMessage": "Get Help" }, - "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { - "defaultMessage": "An error occurred while updating the Get Help settings." - }, - "course.admin.CodaveriSettings.enableDisableButton": { - "defaultMessage": "{enabled, select, true {Enable} other {Disable}}" - }, - "course.admin.CodaveriSettings.enableDisableEvaluator": { - "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Codaveri Evaluator for {questionCount} programming questions in {title}?" + "course.admin.CodaveriSettings.maxGetHelpUserMessages": { + "defaultMessage": "Maximum messages per question" }, - "course.admin.CodaveriSettings.enableDisableLiveFeedback": { - "defaultMessage": "{enabled, select, true {Enable } other {Disable }} Get Help for {questionCount} programming questions in {title}?" + "course.admin.CodaveriSettings.programmingQuestionSettings": { + "defaultMessage": "Programming Question Settings" }, - "course.admin.CodaveriSettings.enableDisableEvaluatorDescription": { - "defaultMessage": "{questionCount} programming questions in this {type} will use {enabled, select, true {Codaveri } other {Default }} evaluator" + "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { + "defaultMessage": "Enable/disable Codaveri as evaluator for programming questions in various assessments." }, - "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { - "defaultMessage": "Get Help for {question} is now {liveFeedbackEnabled, select, true {enabled} other {disabled}}" + "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { + "defaultMessage": "Successfully updated all questions to use {evaluator} evaluator" }, "course.admin.CodaveriSettings.successfulUpdateAllLiveFeedbackEnabled": { "defaultMessage": "Successfully {liveFeedbackEnabled, select, true {enabled} other {disabled}} Get Help for all questions" @@ -647,9 +650,15 @@ "course.admin.ComponentSettings.errorOccurredWhenUpdatingComponents": { "defaultMessage": "An error occurred while updating the component settings." }, + "course.admin.ComponentSettings.settingUpComponent": { + "defaultMessage": "Setting up component for this course" + }, "course.admin.CourseSettings.allowUsersToSendEnrolmentRequests": { "defaultMessage": "Allow users to send enrolment requests" }, + "course.admin.CourseSettings.autoApproveEnrolmentRequests": { + "defaultMessage": "Automatically approve enrolment requests" + }, "course.admin.CourseSettings.clearChanges": { "defaultMessage": "Clear changes" }, @@ -677,6 +686,12 @@ "course.admin.CourseSettings.courseSettings": { "defaultMessage": "Course settings" }, + "course.admin.CourseSettings.courseSuspensionMessage": { + "defaultMessage": "Course suspension message" + }, + "course.admin.CourseSettings.courseSuspensionMessageDescription": { + "defaultMessage": "This message will be shown to users while this course is suspended. Leave blank to show a default message." + }, "course.admin.CourseSettings.daysInAdvance": { "defaultMessage": "Days in advance" }, @@ -764,23 +779,14 @@ "course.admin.CourseSettings.stragglersDescription": { "defaultMessage": "Leave no one behind; subsequent closing reference timings will be pushed back if students complete their assessments late." }, - "course.admin.CourseSettings.suspension": { - "defaultMessage": "Access suspension" - }, "course.admin.CourseSettings.suspendCourse": { "defaultMessage": "Suspend course" }, "course.admin.CourseSettings.suspendCourseDescription": { "defaultMessage": "A suspended course is inaccessible to all students. Instructors can still access the course and all student data will be retained." }, - "course.admin.CourseSettings.unsuspendCourse": { - "defaultMessage": "Unsuspend course" - }, - "course.admin.CourseSettings.courseSuspensionMessage": { - "defaultMessage": "Course suspension message" - }, - "course.admin.CourseSettings.courseSuspensionMessageDescription": { - "defaultMessage": "This message will be shown to users while this course is suspended. Leave blank to show a default message." + "course.admin.CourseSettings.suspendCourseFailure": { + "defaultMessage": "An error occurred while suspending this course." }, "course.admin.CourseSettings.suspendCoursePromptText": { "defaultMessage": "Are you sure you want to suspend this course? All students will not be able to access it until it is unsuspended." @@ -788,20 +794,8 @@ "course.admin.CourseSettings.suspendCourseSuccess": { "defaultMessage": "This course has been suspended." }, - "course.admin.CourseSettings.suspendCourseFailure": { - "defaultMessage": "An error occurred while suspending this course." - }, - "course.admin.CourseSettings.unsuspendCourseSuccess": { - "defaultMessage": "This course has been unsuspended." - }, - "course.admin.CourseSettings.unsuspendCourseFailure": { - "defaultMessage": "An error occurred while unsuspending this course." - }, - "course.admin.CourseSettings.userSuspensionMessage": { - "defaultMessage": "User suspension message" - }, - "course.admin.CourseSettings.userSuspensionMessageDescription": { - "defaultMessage": "This message will be shown to individual users whose access to this course has been suspended. Leave blank to show a default message." + "course.admin.CourseSettings.suspension": { + "defaultMessage": "Access suspension" }, "course.admin.CourseSettings.timeSettings": { "defaultMessage": "Time settings" @@ -812,12 +806,27 @@ "course.admin.CourseSettings.titleRequired": { "defaultMessage": "Course name is required." }, + "course.admin.CourseSettings.unsuspendCourse": { + "defaultMessage": "Unsuspend course" + }, + "course.admin.CourseSettings.unsuspendCourseFailure": { + "defaultMessage": "An error occurred while unsuspending this course." + }, + "course.admin.CourseSettings.unsuspendCourseSuccess": { + "defaultMessage": "This course has been unsuspended." + }, "course.admin.CourseSettings.uploadANewImage": { "defaultMessage": "Choose a new image" }, "course.admin.CourseSettings.uploadingLogo": { "defaultMessage": "Uploading your new logo..." }, + "course.admin.CourseSettings.userSuspensionMessage": { + "defaultMessage": "User suspension message" + }, + "course.admin.CourseSettings.userSuspensionMessageDescription": { + "defaultMessage": "This message will be shown to individual users whose access to this course has been suspended. Leave blank to show a default message." + }, "course.admin.CourseSettingst.confirmDeletePlaceholder": { "defaultMessage": "This is your last chance to go back!" }, @@ -872,6 +881,15 @@ "course.admin.ForumsSettings.markPostAsAnswerSetting": { "defaultMessage": "User who can mark a post as answer" }, + "course.admin.GradebookSettings.gradebookSettings": { + "defaultMessage": "Gradebook settings" + }, + "course.admin.GradebookSettings.weightedViewEnabled": { + "defaultMessage": "Enable weighted grade view" + }, + "course.admin.GradebookSettings.weightedViewEnabledHint": { + "defaultMessage": "Enables a \"Weighted total\" view in the gradebook where staff can configure per-tab weights and see a weighted Total column." + }, "course.admin.LeaderboardSettings.displayUserCount": { "defaultMessage": "Display user count" }, @@ -924,7 +942,10 @@ "defaultMessage": "Component Item Settings" }, "course.admin.LessonPlanSettings.lessonPlanItemSettings": { - "defaultMessage": "Lesson Plan Item Settings" + "defaultMessage": "Item Settings" + }, + "course.admin.LessonPlanSettings.lessonPlanSettings": { + "defaultMessage": "Lesson Plan Settings" }, "course.admin.LessonPlanSettings.noLessonPlanItems": { "defaultMessage": "There are no lesson plan items to configure for lesson plan display." @@ -941,11 +962,14 @@ "course.admin.MaterialSettings.materialsSettings": { "defaultMessage": "Materials settings" }, + "course.admin.NotificationSettings.component": { + "defaultMessage": "Component" + }, "course.admin.NotificationSettings.description": { "defaultMessage": "Description" }, "course.admin.NotificationSettings.emailSettings": { - "defaultMessage": "Email Settings" + "defaultMessage": "Email settings" }, "course.admin.NotificationSettings.noEmailSettings": { "defaultMessage": "None of the enabled components have email settings." @@ -1058,19 +1082,139 @@ "course.admin.NotificationSettings.updateSuccess": { "defaultMessage": "The email setting \"{setting}\" for {user} users has been {action}." }, - "course.admin.SidebarSettings.errorOccurredWhenUpdatingSidebar": { - "defaultMessage": "An error occurred while updating the sidebar ordering." + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.addFailure": { + "defaultMessage": "{forum} could not be added to knowledge base." }, - "course.admin.SidebarSettings.sidebarSettings": { - "defaultMessage": "Student's sidebar ordering" + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.addSuccess": { + "defaultMessage": "{forum} {n, plural, one {has} other {have}} been added to knowledge base." }, - "course.admin.SidebarSettings.sidebarSettingsSubtitle": { - "defaultMessage": "Drag and drop the sidebar items to rearrange." + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.pendingImport": { + "defaultMessage": "Please wait as your request to import forums into knowledge base is being processed. You may close this window while importing is in progress." }, - "course.admin.SidebarSettings.sidebarSettingsUpdated": { - "defaultMessage": "The new sidebar ordering has been applied. Refresh to see the latest changes." + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.removeFailure": { + "defaultMessage": "{forum} could not be removed from knowledge base." }, - "course.admin.VideosSettings.addATab": { + "course.admin.RagWiseSettings.ForumKnowledgeBaseSwitch.removeSuccess": { + "defaultMessage": "{forum} {n, plural, one {has} other {have}} been removed from knowledge base." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.addFailure": { + "defaultMessage": "{material} could not be added to knowledge base." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.addSuccess": { + "defaultMessage": "{material} {n, plural, one {has} other {have}} been added to knowledge base." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.pendingAdd": { + "defaultMessage": "Please wait as your request to add materials into knowledge base is being processed. You may close this window while adding is in progress." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.removeFailure": { + "defaultMessage": "{material} could not be removed from knowledge base." + }, + "course.admin.RagWiseSettings.KnowledgeBaseSwitch.removeSuccess": { + "defaultMessage": "{material} {n, plural, one {has} other {have}} been removed from knowledge base." + }, + "course.admin.RagWiseSettings.expandAll": { + "defaultMessage": "Expand all {object}" + }, + "course.admin.RagWiseSettings.forumSectionSubtitle": { + "defaultMessage": "Manage the inclusion or exclusion of forum data from related courses in the knowledge base, allowing users to control its availability to the LLM for generating responses." + }, + "course.admin.RagWiseSettings.forumSectionTitle": { + "defaultMessage": "No related courses found." + }, + "course.admin.RagWiseSettings.knowledgeBaseStatusSettings": { + "defaultMessage": "Knowledge Base" + }, + "course.admin.RagWiseSettings.materialsSectionSubtitle": { + "defaultMessage": "Add/remove pdf/docx/ipynb/txt files in knowledge base, allowing users to control its availability to the LLM for generating responses." + }, + "course.admin.RagWiseSettings.materialsSectionTitle": { + "defaultMessage": "Materials" + }, + "course.admin.RagWiseSettings.ragWiseSettings": { + "defaultMessage": "RagWise settings" + }, + "course.admin.RagWiseSettings.ragWiseSettingsSubtitle": { + "defaultMessage": "This is currently an experimental feature. RagWise uses Retrieval-Augmented Generation to generate contextually aware responses to student's query on forum." + }, + "course.admin.RagWiseSettings.responseWorkflowAuto": { + "defaultMessage": "Automatically respond" + }, + "course.admin.RagWiseSettings.responseWorkflowDescription": { + "defaultMessage": "When students post a question on forum," + }, + "course.admin.RagWiseSettings.responseWorkflowDraft": { + "defaultMessage": "Always draft" + }, + "course.admin.RagWiseSettings.responseWorkflowDraftDescription": { + "defaultMessage": "Generated response will be drafted." + }, + "course.admin.RagWiseSettings.responseWorkflowHighTrust": { + "defaultMessage": "High trust" + }, + "course.admin.RagWiseSettings.responseWorkflowLowTrust": { + "defaultMessage": "Low trust" + }, + "course.admin.RagWiseSettings.responseWorkflowLowTrustDescription": { + "defaultMessage": "Generated response will be conditionally published with {trust}% trust." + }, + "course.admin.RagWiseSettings.responseWorkflowNoAuto": { + "defaultMessage": "Do not automatically respond" + }, + "course.admin.RagWiseSettings.responseWorkflowPublish": { + "defaultMessage": "Always publish" + }, + "course.admin.RagWiseSettings.responseWorkflowPublishDescription": { + "defaultMessage": "Generated response will be immediately published." + }, + "course.admin.RagWiseSettings.responseWorkflowTitle": { + "defaultMessage": "Automatic Forum Response" + }, + "course.admin.RagWiseSettings.roleplayCharacter": { + "defaultMessage": "Specified Character Prompt" + }, + "course.admin.RagWiseSettings.roleplayCharacterLabel": { + "defaultMessage": "Character prompt (Max 200 Characters)" + }, + "course.admin.RagWiseSettings.roleplayDeadpool": { + "defaultMessage": "You must always impersonate Deadpool character in all your responses." + }, + "course.admin.RagWiseSettings.roleplayDeadpoolLabel": { + "defaultMessage": "Deadpool" + }, + "course.admin.RagWiseSettings.roleplayDescription": { + "defaultMessage": "Customise character prompt to change how LLM response" + }, + "course.admin.RagWiseSettings.roleplayNormal": { + "defaultMessage": "" + }, + "course.admin.RagWiseSettings.roleplayNormalLabel": { + "defaultMessage": "No roleplay" + }, + "course.admin.RagWiseSettings.roleplaySubtitle": { + "defaultMessage": "Character that LLM will roleplay as in responses." + }, + "course.admin.RagWiseSettings.roleplayTitle": { + "defaultMessage": "Response Roleplay" + }, + "course.admin.RagWiseSettings.roleplayYoda": { + "defaultMessage": "You must always impersonate Master Yoda character in all your responses." + }, + "course.admin.RagWiseSettings.roleplayYodaLabel": { + "defaultMessage": "Master Yoda" + }, + "course.admin.SidebarSettings.errorOccurredWhenUpdatingSidebar": { + "defaultMessage": "An error occurred while updating the sidebar ordering." + }, + "course.admin.SidebarSettings.sidebarSettings": { + "defaultMessage": "Student's sidebar ordering" + }, + "course.admin.SidebarSettings.sidebarSettingsSubtitle": { + "defaultMessage": "Drag and drop the sidebar items to rearrange." + }, + "course.admin.SidebarSettings.sidebarSettingsUpdated": { + "defaultMessage": "The new sidebar ordering has been applied. Refresh to see the latest changes." + }, + "course.admin.VideosSettings.addATab": { "defaultMessage": "Add a tab" }, "course.admin.VideosSettings.deleteTabPromptAction": { @@ -1121,6 +1265,51 @@ "course.admin.courseSettings": { "defaultMessage": "Course Settings" }, + "course.admin.storiesSettings.autoCreateAccounts": { + "defaultMessage": "User accounts and chat rooms on Cikgo will automatically be created if they don't yet exist. Information shared with Cikgo is governed by our Privacy Policy and Cikgo's Privacy Policy." + }, + "course.admin.storiesSettings.integrationHint": { + "defaultMessage": "To integrate your course on Cikgo with this course, enter its integration key here. Here's what's going to happen once this course is integrated with Cikgo." + }, + "course.admin.storiesSettings.integrationSettings": { + "defaultMessage": "Integration settings" + }, + "course.admin.storiesSettings.learnTitle": { + "defaultMessage": "Learn page title" + }, + "course.admin.storiesSettings.leaveEmptyToUseDefaultTitle": { + "defaultMessage": "Leave empty to use the default \"Learn\" title." + }, + "course.admin.storiesSettings.onlyOwnersCanManage": { + "defaultMessage": "Only you, Owners, and Managers can configure the integration of this course with Cikgo." + }, + "course.admin.storiesSettings.pingError": { + "defaultMessage": "There was a problem connecting to Cikgo. You may try again at a later time." + }, + "course.admin.storiesSettings.publishTaskCompletions": { + "defaultMessage": "Student's submission statuses will be reflected in their chat rooms in Cikgo." + }, + "course.admin.storiesSettings.pushKey": { + "defaultMessage": "Integration key" + }, + "course.admin.storiesSettings.pushKeyError": { + "defaultMessage": "This integration key doesn't point to a valid course on Cikgo. Please check your settings on Cikgo and try again." + }, + "course.admin.storiesSettings.pushKeyHint": { + "defaultMessage": "Integration keys aren't strictly secretive, but should be handled in confidence." + }, + "course.admin.storiesSettings.pushKeyPointsToCourse": { + "defaultMessage": "This integration key points to {course} on Cikgo." + }, + "course.admin.storiesSettings.redirects": { + "defaultMessage": "When students access this course's root URL, they'll be redirected to the Learn page. The home page is still accessible from the sidebar." + }, + "course.admin.storiesSettings.storiesSettings": { + "defaultMessage": "Stories settings" + }, + "course.admin.storiesSettings.syncs": { + "defaultMessage": "Published assessments, videos, and surveys in this course will be available in and kept in sync with Cikgo as resources." + }, "course.announcement.AnnouncementsDisplay.searchBarPlaceholder": { "defaultMessage": "Search by title or content" }, @@ -1232,6 +1421,12 @@ "course.assessment.AssessmentForm.blockStudentViewingAfterSubmittedHint": { "defaultMessage": "Students will only be able to view their submissions after their grades have been published." }, + "course.assessment.AssessmentForm.blocksAccessesFromInvalidSUS": { + "defaultMessage": "Block accesses from browsers with invalid UA" + }, + "course.assessment.AssessmentForm.blocksAccessesFromInvalidSUSHint": { + "defaultMessage": "If enabled, examinees using browsers with invalid UA (does not contain the specified SUS below) will be blocked from accessing this assessment. Instructors can override access with the session unlock password. Heartbeats from an overridden browser session will be flagged as valid in the PulseGrid." + }, "course.assessment.AssessmentForm.bonusEndAt": { "defaultMessage": "Bonus ends at" }, @@ -1244,9 +1439,6 @@ "course.assessment.AssessmentForm.delayedGradePublicationHint": { "defaultMessage": "If enabled, gradings will not be immediately shown to students. To publish all gradings, you may click Publish Grades in the Submissions page." }, - "course.assessment.AssessmentForm.canEnableCodaveriInComponents": { - "defaultMessage": "Contact the course manager or owner to enable this feature in Components in the Course Settings." - }, "course.assessment.AssessmentForm.description": { "defaultMessage": "Description" }, @@ -1301,12 +1493,24 @@ "course.assessment.AssessmentForm.hasPersonalTimesHint": { "defaultMessage": "Timings for this item will be automatically adjusted for users based on learning rate." }, + "course.assessment.AssessmentForm.hasTimeLimit": { + "defaultMessage": "Automatically submit when timer ends" + }, + "course.assessment.AssessmentForm.hasTimeLimitHint": { + "defaultMessage": "When enabled, each submission will have its own timer and will automatically be finalised when its timer ends." + }, "course.assessment.AssessmentForm.hasToBeMoreThanMinInterval": { "defaultMessage": "Has to be greater than the minimum value." }, "course.assessment.AssessmentForm.hasToBeMoreThanValueMs": { "defaultMessage": "Has to be at least 3000 ms." }, + "course.assessment.AssessmentForm.hasToBeNumber": { + "defaultMessage": "Has to be valid number." + }, + "course.assessment.AssessmentForm.hasToBePositive": { + "defaultMessage": "Has to be positive." + }, "course.assessment.AssessmentForm.hasToBePositiveInteger": { "defaultMessage": "Has to be a positive integer less than 86,400,000 ms" }, @@ -1319,6 +1523,12 @@ "course.assessment.AssessmentForm.intervalHint": { "defaultMessage": "Controls how frequent heartbeats are sent from the students' browsers. Intervals are randomised between these two ranges." }, + "course.assessment.AssessmentForm.koditsuDisabledInCourse": { + "defaultMessage": "Please contact the Course Administrator to enable Koditsu Exam in Course Settings." + }, + "course.assessment.AssessmentForm.liveFeedback": { + "defaultMessage": "Get Help" + }, "course.assessment.AssessmentForm.maxInterval": { "defaultMessage": "Max interval" }, @@ -1328,9 +1538,18 @@ "course.assessment.AssessmentForm.minInterval": { "defaultMessage": "Min interval" }, + "course.assessment.AssessmentForm.minutes": { + "defaultMessage": "minute(s)" + }, "course.assessment.AssessmentForm.modeSwitchingHint": { "defaultMessage": "You can no longer change the grading mode because there are already submissions for this assessment." }, + "course.assessment.AssessmentForm.needSUSAndSessionUnlockPassword": { + "defaultMessage": "You need to specify a SUS and session unlock password to enable this." + }, + "course.assessment.AssessmentForm.noProgrammingQuestion": { + "defaultMessage": "You need to add at least one programming question that can be supported by Codaveri to allow enabling Get Help for this Assessment" + }, "course.assessment.AssessmentForm.noTestCaseChosenError": { "defaultMessage": "Select at least one type of test case" }, @@ -1355,29 +1574,32 @@ "course.assessment.AssessmentForm.personalisedTimelines": { "defaultMessage": "Personalised timelines" }, + "course.assessment.AssessmentForm.proctorWithKoditsu": { + "defaultMessage": "Proctor Exam using Koditsu" + }, "course.assessment.AssessmentForm.published": { "defaultMessage": "Published" }, "course.assessment.AssessmentForm.publishedHint": { "defaultMessage": "Everyone can see this assessment." }, + "course.assessment.AssessmentForm.questionsIncompatibleWithKoditsu": { + "defaultMessage": "Please make sure that all questions in this assessment is compatible with Koditsu before activating proctoring in Koditsu" + }, "course.assessment.AssessmentForm.secret": { "defaultMessage": "Secret UA Substring (SUS)" }, "course.assessment.AssessmentForm.secretHint": { - "defaultMessage": "If provided, Coursemology can automatically flag a connection as valid in PulseGrid if the examinee's User Agent (UA) contains this secret. Otherwise, connections will be flagged only by heartbeat intervals." + "defaultMessage": "If provided, the PulseGrid automatically checks if the examinee's browser's User Agent (UA) contains this secret, and marks connections that do not as invalid. This string is case-sensitive." }, "course.assessment.AssessmentForm.sessionPassword": { "defaultMessage": "Session unlock password" }, - "course.assessment.AssessmentForm.sessionPasswordHint": { - "defaultMessage": "Ideally, do NOT give this password to students." - }, "course.assessment.AssessmentForm.sessionProtection": { "defaultMessage": "Enable session protection" }, "course.assessment.AssessmentForm.sessionProtectionHint": { - "defaultMessage": "If enabled, students can only access their attempt once. Further access will require the session unlock password." + "defaultMessage": "If enabled, students can only access their attempt once. Further access will require the session unlock password. Ideally, do NOT give this password to students." }, "course.assessment.AssessmentForm.showEvaluation": { "defaultMessage": "Show evaluation test cases" @@ -1391,12 +1613,12 @@ "course.assessment.AssessmentForm.showMcqMrqSolution": { "defaultMessage": "Show MCQ/MRQ solution(s)" }, - "course.assessment.AssessmentForm.showRubricToStudents": { - "defaultMessage": "Show rubric breakdown to students" - }, "course.assessment.AssessmentForm.showPrivate": { "defaultMessage": "Show private test cases" }, + "course.assessment.AssessmentForm.showRubricToStudents": { + "defaultMessage": "Show rubric breakdown to students" + }, "course.assessment.AssessmentForm.singlePage": { "defaultMessage": "Single Page" }, @@ -1421,9 +1643,15 @@ "course.assessment.AssessmentForm.timeBonusExp": { "defaultMessage": "Time Bonus EXP" }, + "course.assessment.AssessmentForm.timeLimit": { + "defaultMessage": "Time Limit" + }, "course.assessment.AssessmentForm.title": { "defaultMessage": "Title" }, + "course.assessment.AssessmentForm.toggleLiveFeedbackDescription": { + "defaultMessage": "Enable Get Help feature for all programming questions" + }, "course.assessment.AssessmentForm.unavailableInAutograded": { "defaultMessage": "Unavailable in autograded assessments." }, @@ -1454,12 +1682,6 @@ "course.assessment.AssessmentForm.visibility": { "defaultMessage": "Visibility" }, - "course.assessment.AssessmentForm.toggleLiveFeedbackDescription": { - "defaultMessage": "{enabled, select, true {Enable} other {Disable}} Get Help feature for all programming questions" - }, - "course.assessment.AssessmentForm.noProgrammingQuestion": { - "defaultMessage": "You need to add at least one programming question that can be supported by Codaveri to allow enabling Get Help for this Assessment" - }, "course.assessment.FileManager.addFiles": { "defaultMessage": "Add Files" }, @@ -1505,9 +1727,24 @@ "course.assessment.edit.update": { "defaultMessage": "Save" }, + "course.assessment.generation.allFieldsLocked": { + "defaultMessage": "All fields are locked, so nothing can be generated." + }, "course.assessment.generation.confirmDeleteConversation": { "defaultMessage": "Are you sure you want to delete \"{title}\" and all its history items? THIS ACTION IS IRREVERSIBLE!" }, + "course.assessment.generation.createMode": { + "defaultMessage": "Create New" + }, + "course.assessment.generation.createModeTooltip": { + "defaultMessage": "Generate fresh questions from scratch" + }, + "course.assessment.generation.enhanceMode": { + "defaultMessage": "Enhance" + }, + "course.assessment.generation.enhanceModeTooltip": { + "defaultMessage": "Build upon your current question" + }, "course.assessment.generation.exportAction": { "defaultMessage": "Export" }, @@ -1517,140 +1754,212 @@ "course.assessment.generation.exportError": { "defaultMessage": "An error occurred in exporting this question: {error}" }, - "course.assessment.generation.lockTooltip": { - "defaultMessage": "Lock to prevent changes to this section" - }, - "course.assessment.generation.newTab": { - "defaultMessage": "New" - }, - "course.assessment.generation.openExportDialog": { - "defaultMessage": "Export" + "course.assessment.generation.generateError": { + "defaultMessage": "An error occurred generating question \"{title}\"." }, - "course.assessment.generation.resetConversation": { - "defaultMessage": "Reset" + "course.assessment.generation.generateMcqPage": { + "defaultMessage": "Generate Multiple Choice Question" }, - "course.assessment.generation.unlockTooltip": { - "defaultMessage": "Unlock to continue editing this section" + "course.assessment.generation.generateMrqPage": { + "defaultMessage": "Generate Multiple Response Question" }, - "course.assessment.generation.mrq.numberOfQuestionsField": { - "defaultMessage": "Number of Questions" + "course.assessment.generation.generateMultipleSuccess": { + "defaultMessage": "Successfully generated {count} questions!" }, - "course.assessment.generation.promptPlaceholder": { - "defaultMessage": "Type something here..." + "course.assessment.generation.generatePage": { + "defaultMessage": "Generate Programming Question" }, "course.assessment.generation.generateQuestion": { "defaultMessage": "Generate" }, - "course.assessment.generation.showInactive": { - "defaultMessage": "Show inactive items" - }, - "course.assessment.generation.mrq.numberOfQuestionsRange": { - "defaultMessage": "Please enter a number from {min} to {max}" + "course.assessment.generation.generateSuccess": { + "defaultMessage": "Generation for \"{title}\" successful." }, - "course.assessment.generation.enhanceMode": { - "defaultMessage": "Enhance" + "course.assessment.generation.languageField": { + "defaultMessage": "Language" }, - "course.assessment.generation.createMode": { - "defaultMessage": "Create New" + "course.assessment.generation.loadingSourceError": { + "defaultMessage": "Unable to load source question data." }, - "course.assessment.generation.enhanceModeTooltip": { - "defaultMessage": "Build upon your current question" + "course.assessment.generation.lockTooltip": { + "defaultMessage": "Lock to prevent changes to this section" }, - "course.assessment.generation.createModeTooltip": { - "defaultMessage": "Generate fresh questions from scratch" + "course.assessment.generation.mrq.exportAction": { + "defaultMessage": "Export" }, "course.assessment.generation.mrq.exportDialogHeader": { "defaultMessage": "Export Questions ({exportCount} selected)" }, - "course.assessment.generation.requireNonEmptyOptionError": { - "defaultMessage": "Question must have at least one non-empty option" + "course.assessment.generation.mrq.numberOfQuestionsField": { + "defaultMessage": "Number of Questions" }, - "course.assessment.generation.untitledQuestion": { - "defaultMessage": "Untitled Question" + "course.assessment.generation.mrq.numberOfQuestionsRange": { + "defaultMessage": "Please enter a number from {min} to {max}" }, - "course.assessment.question.multipleResponses.showOptions": { - "defaultMessage": "Show Options" + "course.assessment.generation.newTab": { + "defaultMessage": "New" }, - "course.assessment.question.multipleResponses.hideOptions": { - "defaultMessage": "Hide Options" + "course.assessment.generation.openExportDialog": { + "defaultMessage": "Export" }, - "course.assessment.question.multipleResponses.noOptions": { - "defaultMessage": "No options" + "course.assessment.generation.promptPlaceholder": { + "defaultMessage": "Type something here..." }, - "course.assessment.question.multipleResponses.title": { - "defaultMessage": "Title" + "course.assessment.generation.requireNonEmptyOptionError": { + "defaultMessage": "Question must have at least one non-empty option" }, - "course.assessment.generation.generateMrqPage": { - "defaultMessage": "Generate Multiple Response Question" + "course.assessment.generation.resetConversation": { + "defaultMessage": "Reset" }, - "course.assessment.generation.generateMcqPage": { - "defaultMessage": "Generate Multiple Choice Question" + "course.assessment.generation.showInactive": { + "defaultMessage": "Show inactive items" }, - "course.assessment.generation.generateMultipleSuccess": { - "defaultMessage": "Successfully generated {count} questions!" + "course.assessment.generation.sourceLanguageNotSupported": { + "defaultMessage": "Source question language not supported by the generation tool." }, - "course.assessment.generation.generateSuccess": { - "defaultMessage": "Generation for {title} successful." + "course.assessment.generation.unlockTooltip": { + "defaultMessage": "Unlock to continue editing this section" }, - "course.assessment.generation.generateError": { - "defaultMessage": "An error occurred generating question {title}." + "course.assessment.generation.untitledQuestion": { + "defaultMessage": "Untitled Question" }, - "course.assessment.generation.loadingSourceError": { - "defaultMessage": "Unable to load source question data." + "course.assessment.liveFeedback.comments": { + "defaultMessage": "Comments" }, - "course.assessment.generation.allFieldsLocked": { - "defaultMessage": "All fields are locked, so nothing can be generated." + "course.assessment.liveFeedback.lineHeader": { + "defaultMessage": "Line {lineNumber}" + }, + "course.assessment.liveFeedback.messageTimingTitle": { + "defaultMessage": "Generated at: {usedAt}" + }, + "course.assessment.liveFeedback.questionTitle": { + "defaultMessage": "Question {index}" + }, + "course.assessment.monitoring.accessGrantedForThisSessionOnly": { + "defaultMessage": "Access will be granted only for this browser session." }, "course.assessment.monitoring.alivePresenceHint": { "defaultMessage": "Last heartbeat was received in time." }, "course.assessment.monitoring.alivePresenceHintSUSMatches": { - "defaultMessage": "Last heartbeat was received in time and the SUS matches." + "defaultMessage": "Last heartbeat was received in time and came from an authorised browser, if browser authorisation is enabled." }, "course.assessment.monitoring.blankField": { "defaultMessage": "(blank)" }, + "course.assessment.monitoring.blocksAccessesFromInvalidSUS": { + "defaultMessage": "Block accesses from unauthorised browsers" + }, + "course.assessment.monitoring.blocksAccessesFromInvalidSUSHint": { + "defaultMessage": "If enabled, examinees using unauthorised browsers can't access this assessment. Instructors can override access with the session unlock password. Heartbeats from overridden browser sessions will always be valid (green) in the PulseGrid." + }, + "course.assessment.monitoring.browserAuthorizationMethod": { + "defaultMessage": "Browser authorisation method" + }, + "course.assessment.monitoring.browserAuthorizationMethodHint": { + "defaultMessage": "Choose how sessions are authorised as valid or invalid. Changes apply to all sessions and heartbeats immediately and updates live in PulseGrid." + }, "course.assessment.monitoring.cannotConnectToLiveMonitoringChannel": { "defaultMessage": "Oops, an error occurred when connecting to the live monitoring channel." }, "course.assessment.monitoring.connected": { "defaultMessage": "Connected" }, - "course.assessment.monitoring.connectedToLiveMonitoringChannel": { - "defaultMessage": "Connected to the live monitoring channel" + "course.assessment.monitoring.connecting": { + "defaultMessage": "Connecting" + }, + "course.assessment.monitoring.deltaFromPreviousHeartbeat": { + "defaultMessage": "{ms} ms from previous heartbeat" }, "course.assessment.monitoring.detailsOfNHeartbeats": { - "defaultMessage": "Details of the last {n} heartbeats" + "defaultMessage": "Last {n} heartbeats" }, "course.assessment.monitoring.disconnected": { "defaultMessage": "Disconnected" }, - "course.assessment.monitoring.disconnectedFromLiveMonitoringChannel": { - "defaultMessage": "Disconnected from the live monitoring channel" + "course.assessment.monitoring.enableBrowserAuthorization": { + "defaultMessage": "Authorise browsers that access this assessment" + }, + "course.assessment.monitoring.enableBrowserAuthorizationHint": { + "defaultMessage": "If enabled, PulseGrid will additionally check if an examinee is accessing this assessment from an authorised browser, based on the authorisation method you choose." + }, + "course.assessment.monitoring.examMonitoring": { + "defaultMessage": "Enable exam monitoring" + }, + "course.assessment.monitoring.examMonitoringHint": { + "defaultMessage": "If enabled, examinees' sessions will be monitored in real time from when they attempt the exam until they finalise it or the first 24 hours since their attempt, whichever is earlier. Instructors can monitor these sessions in PulseGrid." + }, + "course.assessment.monitoring.expiredSession": { + "defaultMessage": "Expired session. It has been at least 24 hours since the submission was made." }, "course.assessment.monitoring.filterByGroup": { "defaultMessage": "Filter by Group" }, + "course.assessment.monitoring.firstReceivedHeartbeat": { + "defaultMessage": "First received heartbeat" + }, "course.assessment.monitoring.generatedAt": { "defaultMessage": "Generated at" }, + "course.assessment.monitoring.intervalHint": { + "defaultMessage": "Controls how frequent heartbeats are sent from the examinees' browsers. Intervals are randomised between these two ranges." + }, + "course.assessment.monitoring.invalidBrowser": { + "defaultMessage": "Invalid browser configuration" + }, + "course.assessment.monitoring.invalidBrowserSubtitle": { + "defaultMessage": "Access to this assessment is not allowed with your current browser and/or its configuration. Contact your instructor for assistance." + }, + "course.assessment.monitoring.invalidHeartbeat": { + "defaultMessage": "Invalid" + }, "course.assessment.monitoring.ipAddress": { "defaultMessage": "IP Address" }, - "course.assessment.monitoring.lastHeartbeat": { - "defaultMessage": "Last heartbeat" - }, "course.assessment.monitoring.latePresenceHint": { "defaultMessage": "Next heartbeat hasn't been received in time, but still within the configured inter-heartbeats interval." }, "course.assessment.monitoring.live": { "defaultMessage": "Live" }, + "course.assessment.monitoring.liveHint": { + "defaultMessage": "This heartbeat was immediately received by the server." + }, + "course.assessment.monitoring.liveness": { + "defaultMessage": "Liveness" + }, + "course.assessment.monitoring.loadAllHeartbeats": { + "defaultMessage": "Load all" + }, + "course.assessment.monitoring.maxInterval": { + "defaultMessage": "Max interval" + }, + "course.assessment.monitoring.milliseconds": { + "defaultMessage": "ms" + }, + "course.assessment.monitoring.minInterval": { + "defaultMessage": "Min interval" + }, "course.assessment.monitoring.missingPresenceHint": { - "defaultMessage": "Next heartbeat hasn't been received in time." + "defaultMessage": "Next heartbeat hasn't been received in time, or the last heartbeat came from an unauthorised browser, if browser authorisation is enabled." + }, + "course.assessment.monitoring.needSUSAndSessionUnlockPassword": { + "defaultMessage": "You must enable browser authorisation and set a session unlock password to enable this." }, "course.assessment.monitoring.noActiveSessions": { - "defaultMessage": "No active sessions." + "defaultMessage": "No active sessions. No attempts have been made." + }, + "course.assessment.monitoring.offset": { + "defaultMessage": "Inter-heartbeat offset" + }, + "course.assessment.monitoring.offsetHint": { + "defaultMessage": "Controls how long PulseGrid should wait after the frequency interval before flagging a session as late." + }, + "course.assessment.monitoring.openSubmissionInNewTab": { + "defaultMessage": "Open submission in new tab" + }, + "course.assessment.monitoring.overrideAccess": { + "defaultMessage": "Override access" }, "course.assessment.monitoring.pulsegrid": { "defaultMessage": "PulseGrid" @@ -1661,27 +1970,120 @@ "course.assessment.monitoring.recentActivitiesHint": { "defaultMessage": "These logs will disappear if you close this tab!" }, - "course.assessment.monitoring.stale": { - "defaultMessage": "Stale" + "course.assessment.monitoring.resetZoom": { + "defaultMessage": "Reset zoom" }, - "course.assessment.monitoring.summaryCorrectAsAt": { - "defaultMessage": "Summary correct as at {time}" + "course.assessment.monitoring.sebConfigKey": { + "defaultMessage": "Safe Exam Browser (SEB) Config Key" }, - "course.assessment.monitoring.type": { - "defaultMessage": "Type" + "course.assessment.monitoring.sebConfigKeyFieldHint": { + "defaultMessage": "Your SEB Config Key, not the Browser Exam Key, is generated from your specific SEB configuration. It stays the same across operating systems and SEB versions. Ensure this field is updated if you change your SEB configuration." }, - "course.assessment.monitoring.userAgent": { - "defaultMessage": "User Agent" + "course.assessment.monitoring.sebConfigKeyFieldLabel": { + "defaultMessage": "SEB Config Key" }, - "course.assessment.monitoring.userHeartbeatContinuedStreaming": { - "defaultMessage": "{name}'s heartbeat just continued streaming." + "course.assessment.monitoring.sebConfigKeyHint": { + "defaultMessage": "Flags a session as valid if the examinee is using Safe Exam Browser (SEB) with a valid configuration. SEB generates a unique Config Key for a specific configuration. This method requires SEB 3.4 for Windows and SEB 3.0 for iOS and macOS, or later." }, - "course.assessment.monitoring.userHeartbeatNotReceivedInTime": { - "defaultMessage": "{name}'s heartbeat wasn't received in time." + "course.assessment.monitoring.sebPayload": { + "defaultMessage": "Safe Exam Browser (SEB) Config Key Hash & URL" + }, + "course.assessment.monitoring.secret": { + "defaultMessage": "Secret UA Substring (SUS)" + }, + "course.assessment.monitoring.secretHint": { + "defaultMessage": "If an examinee's browser's User Agent (UA) contains this case-sensitive secret, PulseGrid will flag that session as valid, and invalid otherwise. If you leave this blank, all sessions will be flagged as valid." + }, + "course.assessment.monitoring.sessionUnlockPassword": { + "defaultMessage": "Session unlock password" + }, + "course.assessment.monitoring.stale": { + "defaultMessage": "Stale" + }, + "course.assessment.monitoring.staleHint": { + "defaultMessage": "This heartbeat wasn't immediately received by the server because the examinee's browser was temporarily unreachable. It was cached in the browser, and sent to the server when the browser was reachable again." + }, + "course.assessment.monitoring.stoppedSession": { + "defaultMessage": "Stopped session. Student may have finalised their submission." + }, + "course.assessment.monitoring.summaryCorrectAsAt": { + "defaultMessage": "Summary correct as at {time}" + }, + "course.assessment.monitoring.type": { + "defaultMessage": "Type" + }, + "course.assessment.monitoring.userAgent": { + "defaultMessage": "User Agent (UA)" + }, + "course.assessment.monitoring.userAgentHint": { + "defaultMessage": "Flags a session as valid if the examinee's browser's User Agent (UA) contains a secret substring." + }, + "course.assessment.monitoring.userHeartbeatContinuedStreaming": { + "defaultMessage": "{name}'s heartbeat just continued streaming." + }, + "course.assessment.monitoring.userHeartbeatNotReceivedInTime": { + "defaultMessage": "{name}'s heartbeat wasn't received in time." + }, + "course.assessment.monitoring.validHeartbeat": { + "defaultMessage": "Valid" + }, + "course.assessment.monitoring.zoomPanHint": { + "defaultMessage": "Pinch or scroll to zoom. Drag to pan." }, "course.assessment.newAssessment": { "defaultMessage": "New Assessment" }, + "course.assessment.plagiarism.actions": { + "defaultMessage": "Actions" + }, + "course.assessment.plagiarism.baseSubmission": { + "defaultMessage": "Base Submission" + }, + "course.assessment.plagiarism.cannotManageSubmission": { + "defaultMessage": "You do not have permission to manage this submission." + }, + "course.assessment.plagiarism.comparedSubmission": { + "defaultMessage": "Compared Submission" + }, + "course.assessment.plagiarism.confirmStartMessage": { + "defaultMessage": "Running a new plagiarism check will remove the previous results." + }, + "course.assessment.plagiarism.confirmStartTitle": { + "defaultMessage": "Confirm Plagiarism Check?" + }, + "course.assessment.plagiarism.downloadPdf": { + "defaultMessage": "Download PDF" + }, + "course.assessment.plagiarism.lastRunTime": { + "defaultMessage": "Last run at: {date}" + }, + "course.assessment.plagiarism.notStarted": { + "defaultMessage": "No plagiarism check has been run" + }, + "course.assessment.plagiarism.plagiarism": { + "defaultMessage": "Plagiarism Results" + }, + "course.assessment.plagiarism.results": { + "defaultMessage": "Plagiarism Results (similarity between submissions)" + }, + "course.assessment.plagiarism.searchByStudentName": { + "defaultMessage": "Search by Student Name" + }, + "course.assessment.plagiarism.showSelfPlagiarism": { + "defaultMessage": "Include self-plagiarism comparisons (same student, different courses)" + }, + "course.assessment.plagiarism.similarityScore": { + "defaultMessage": "Similarity Score" + }, + "course.assessment.plagiarism.start": { + "defaultMessage": "New Plagiarism Check" + }, + "course.assessment.plagiarism.status": { + "defaultMessage": "Plagiarism Check Status" + }, + "course.assessment.plagiarism.viewReport": { + "defaultMessage": "View Report" + }, "course.assessment.question.forumPostResponses.enableTextResponse": { "defaultMessage": "Include a text field for students to provide further inputs" }, @@ -1754,6 +2156,9 @@ "course.assessment.question.multipleResponses.grading": { "defaultMessage": "Grading" }, + "course.assessment.question.multipleResponses.hideOptions": { + "defaultMessage": "Hide Options" + }, "course.assessment.question.multipleResponses.ignoresRandomization": { "defaultMessage": "Ignores randomization" }, @@ -1766,18 +2171,39 @@ "course.assessment.question.multipleResponses.maximumGrade": { "defaultMessage": "Maximum grade" }, + "course.assessment.question.multipleResponses.mustBeLessThanMaxAttachmentSize": { + "defaultMessage": "Must be at most {defaultMax}MB." + }, + "course.assessment.question.multipleResponses.mustBeLessThanMaxAttachments": { + "defaultMessage": "Must be at most {defaultMax}." + }, "course.assessment.question.multipleResponses.mustBeLessThanMaxMaximumGrade": { "defaultMessage": "Must be less than 1000." }, + "course.assessment.question.multipleResponses.mustHaveAtLeastOneResponse": { + "defaultMessage": "You must specify at least one response." + }, "course.assessment.question.multipleResponses.mustSpecifyAtLeastOneCorrectChoice": { "defaultMessage": "You must specify at least one correct choice." }, "course.assessment.question.multipleResponses.mustSpecifyChoice": { "defaultMessage": "You must specify a valid choice title." }, + "course.assessment.question.multipleResponses.mustSpecifyMaxAttachment": { + "defaultMessage": "You must specify a valid, positive maximum attachment number." + }, + "course.assessment.question.multipleResponses.mustSpecifyMaxAttachmentSize": { + "defaultMessage": "You must specify a valid, positive maximum attachment size." + }, "course.assessment.question.multipleResponses.mustSpecifyMaximumGrade": { "defaultMessage": "You must specify a valid, non-negative maximum grade to award." }, + "course.assessment.question.multipleResponses.mustSpecifyPositiveMaxAttachment": { + "defaultMessage": "Max Number of Attachments has to be at least 2." + }, + "course.assessment.question.multipleResponses.mustSpecifyPositiveMaxAttachmentSize": { + "defaultMessage": "Max Size has to be positive." + }, "course.assessment.question.multipleResponses.mustSpecifyPositiveMaximumGrade": { "defaultMessage": "Maximum grade has to be non-negative." }, @@ -1790,6 +2216,9 @@ "course.assessment.question.multipleResponses.newResponseCannotUndo": { "defaultMessage": "This is a new response. It will immediately disappear if you delete before saving it." }, + "course.assessment.question.multipleResponses.noOptions": { + "defaultMessage": "No options" + }, "course.assessment.question.multipleResponses.noSkillsCanCreateSkills": { "defaultMessage": "There are no skills in this course yet. You can create new skills at the Skills page." }, @@ -1826,6 +2255,9 @@ "course.assessment.question.multipleResponses.saveChangesFirstBeforeConvertingMcqMrq": { "defaultMessage": "Please save your changes before attempting to convert this question." }, + "course.assessment.question.multipleResponses.showOptions": { + "defaultMessage": "Show Options" + }, "course.assessment.question.multipleResponses.skills": { "defaultMessage": "Skills" }, @@ -1838,6 +2270,9 @@ "course.assessment.question.multipleResponses.staffOnlyCommentsHint": { "defaultMessage": "Useful for internal notes or documentations. Students will never see this." }, + "course.assessment.question.multipleResponses.title": { + "defaultMessage": "Title" + }, "course.assessment.question.multipleResponses.undoDeleteChoice": { "defaultMessage": "Undo delete choice" }, @@ -1892,6 +2327,9 @@ "course.assessment.question.programming.codaveriEvaluatorHint": { "defaultMessage": "On top of the default evaluation, this evaluator will provide automated code feedback powered by Codaveri when the submission is finalised. They will appear as draft comments for the instructors to review, edit, and publish." }, + "course.assessment.question.programming.codaveriEvaluatorNotSupported": { + "defaultMessage": "{languageName} is not supported by the Codaveri evaluator." + }, "course.assessment.question.programming.codeInserts": { "defaultMessage": "Code inserts" }, @@ -1913,15 +2351,18 @@ "course.assessment.question.programming.defaultEvaluator": { "defaultMessage": "Default" }, - "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { - "defaultMessage": "{name}: Installed Dependencies" - }, "course.assessment.question.programming.defaultEvaluatorDependencyDescription": { "defaultMessage": "Submitted code is run in a containerized environment with the following dependencies installed locally.{br}If your programming question requires a dependency not listed below, contact us and we will consider adding it." }, + "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { + "defaultMessage": "{name}: Installed Dependencies" + }, "course.assessment.question.programming.defaultEvaluatorHint": { "defaultMessage": "No fuss; just run the code according to the evaluation package below and report the test results." }, + "course.assessment.question.programming.defaultEvaluatorNotSupported": { + "defaultMessage": "{languageName} is not supported by the default evaluator." + }, "course.assessment.question.programming.dependencySearchText": { "defaultMessage": "Search dependencies by name" }, @@ -1953,7 +2394,7 @@ "defaultMessage": "Hold tight, evaluating all submissions with the new package..." }, "course.assessment.question.programming.evaluationLimits": { - "defaultMessage": "Evaluationlimits" + "defaultMessage": "Evaluation limits" }, "course.assessment.question.programming.evaluationTestCases": { "defaultMessage": "Evaluation test cases" @@ -1970,6 +2411,9 @@ "course.assessment.question.programming.expected": { "defaultMessage": "Expected" }, + "course.assessment.question.programming.expectedOutput": { + "defaultMessage": "Expected Output" + }, "course.assessment.question.programming.expression": { "defaultMessage": "Expression" }, @@ -2000,6 +2444,9 @@ "course.assessment.question.programming.inlineCode": { "defaultMessage": "Inline code" }, + "course.assessment.question.programming.input": { + "defaultMessage": "Input" + }, "course.assessment.question.programming.javaTestCasesHint": { "defaultMessage": "Expressions will be evaluated in the context of the submitted code. Their return values will be compared against the Expected expectations using the expectEquals(expression, expected) void. Its simplified definition is as follows, where Object has been overloaded for all Java primitives." }, @@ -2015,6 +2462,9 @@ "course.assessment.question.programming.languageAndEvaluation": { "defaultMessage": "Language and evaluation" }, + "course.assessment.question.programming.languageDeprecatedWarning": { + "defaultMessage": "Your selected language is deprecated. Please change it to another language." + }, "course.assessment.question.programming.lastUpdated": { "defaultMessage": "Last updated by {by} on {on}." }, @@ -2024,6 +2474,9 @@ "course.assessment.question.programming.liveFeedbackCustomPromptDescription": { "defaultMessage": "Add instructions to guide the generation of Get Help feedback here. If unsure, just leave this blank." }, + "course.assessment.question.programming.liveFeedbackNotSupported": { + "defaultMessage": "Get Help is not supported for {languageName}." + }, "course.assessment.question.programming.lowestGradingPriority": { "defaultMessage": "Lowest grading priority" }, @@ -2051,24 +2504,24 @@ "course.assessment.question.programming.packageCreationModeHint": { "defaultMessage": "You cannot change this mode once this question is successfully created. Choose wisely!" }, - "course.assessment.question.programming.packageImportSuccess": { - "defaultMessage": "The package was successfully imported." + "course.assessment.question.programming.packageImportEvaluationError": { + "defaultMessage": "An error occurred evaluating your solution against its test cases. Please double-check them and try again." + }, + "course.assessment.question.programming.packageImportEvaluationTimeout": { + "defaultMessage": "No response was received from an evaluator within the required time. This may indicate all our evaluators are busy right now, please try again later." + }, + "course.assessment.question.programming.packageImportGenericError": { + "defaultMessage": "The package could not be imported: {error}" }, "course.assessment.question.programming.packageImportInvalidPackage": { "defaultMessage": "The package could not be imported: the uploaded package does not have a valid structure." }, - "course.assessment.question.programming.packageImportEvaluationTimeout": { - "defaultMessage": "No response was received from an evaluator within the required time. This may indicate all our evaluators are busy right now, please try again later." + "course.assessment.question.programming.packageImportSuccess": { + "defaultMessage": "The package was successfully imported." }, "course.assessment.question.programming.packageImportTimeLimitExceeded": { "defaultMessage": "The solution did not finish evaluating the test cases in the specified time limit." }, - "course.assessment.question.programming.packageImportEvaluationError": { - "defaultMessage": "An error occurred evaluating your solution against its test cases. Please double-check them and try again." - }, - "course.assessment.question.programming.packageImportGenericError": { - "defaultMessage": "The package could not be imported: {error}" - }, "course.assessment.question.programming.packageInfoOnline": { "defaultMessage": "Generated evaluation package" }, @@ -2129,6 +2582,9 @@ "course.assessment.question.programming.standardError": { "defaultMessage": "Standard error" }, + "course.assessment.question.programming.standardInputOutputTestCasesHint": { + "defaultMessage": "Each test case launches a separate {language} console environment and provides input via standard input. The environment will combine the Prepend, student submission, and Append scripts into a single program and run it. The standard output of the program will be compared (as a string) to the expected output of the test case. We recommend handling input parsing and function calls in one of these scripts." + }, "course.assessment.question.programming.standardOutput": { "defaultMessage": "Standard output" }, @@ -2156,6 +2612,9 @@ "course.assessment.question.programming.timeLimit": { "defaultMessage": "Time limit" }, + "course.assessment.question.programming.timeLimitDetail": { + "defaultMessage": "{timeLimit, plural, one {# minute} other {# minutes}}" + }, "course.assessment.question.programming.uploadNewPackage": { "defaultMessage": "Upload a new package" }, @@ -2171,164 +2630,242 @@ "course.assessment.question.programminquestion.questionSavedRedirecting": { "defaultMessage": "Question saved." }, - "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { - "defaultMessage": "Cannot be blank." + "course.assessment.question.rubricBasedResponses.addNewCategory": { + "defaultMessage": "Add new category" }, - "course.assessment.question.scribing.ScribingQuestionForm.chooseFileButton": { - "defaultMessage": "Choose File", - "description": "Button for adding an image attachment." + "course.assessment.question.rubricBasedResponses.addNewLevel": { + "defaultMessage": "Add new grade" }, - "course.assessment.question.scribing.ScribingQuestionForm.descriptionFieldLabel": { - "defaultMessage": "Description", - "description": "Label for the description input field." + "course.assessment.question.rubricBasedResponses.aiGrading": { + "defaultMessage": "AI Grading" }, - "course.assessment.question.scribing.ScribingQuestionForm.fetchFailureMessage": { - "defaultMessage": "An error occurred, please try again." + "course.assessment.question.rubricBasedResponses.aiGradingCustomPrompt": { + "defaultMessage": "Custom Prompt" }, - "course.assessment.question.scribing.ScribingQuestionForm.fileAttachmentRequired": { - "defaultMessage": "File attachment required." + "course.assessment.question.rubricBasedResponses.aiGradingCustomPromptDescription": { + "defaultMessage": "Add grading instructions (e.g. question context, model answer, feedback tone). Leave blank if unsure." }, - "course.assessment.question.scribing.ScribingQuestionForm.fileUploaded": { - "defaultMessage": "File uploaded:" + "course.assessment.question.rubricBasedResponses.aiGradingModelAnswer": { + "defaultMessage": "Model Answer" }, - "course.assessment.question.scribing.ScribingQuestionForm.lessThanEqualZeroValidationError": { - "defaultMessage": "Value must be greater than 0." + "course.assessment.question.rubricBasedResponses.aiGradingModelAnswerDescription": { + "defaultMessage": "Add an example answer that would get the maximum grades in each rubric category. Leave blank if unsure." }, - "course.assessment.question.scribing.ScribingQuestionForm.maximumGradeFieldLabel": { - "defaultMessage": "Maximum Grade", - "description": "Label for the maximum grade input field." + "course.assessment.question.rubricBasedResponses.bonusReservedNames": { + "defaultMessage": "After finalization, a special category named 'Moderation' will be added automatically. It allows graders to award bonus or penalty points at their discretion." }, - "course.assessment.question.scribing.ScribingQuestionForm.noFileChosenMessage": { - "defaultMessage": "No file chosen", - "description": "Message to be displayed when no file is chosen for a file input." + "course.assessment.question.rubricBasedResponses.categoryGrade": { + "defaultMessage": "Grade" }, - "course.assessment.question.scribing.ScribingQuestionForm.positiveNumberValidationError": { - "defaultMessage": "Value must be positive." + "course.assessment.question.rubricBasedResponses.categoryGradeExplanation": { + "defaultMessage": "Explanation" }, - "course.assessment.question.scribing.ScribingQuestionForm.resolveErrorsMessage": { - "defaultMessage": "This form has errors, please resolve before submitting." + "course.assessment.question.rubricBasedResponses.categoryMaximumGrade": { + "defaultMessage": "Max" }, - "course.assessment.question.scribing.ScribingQuestionForm.scribingQuestionWarning": { - "defaultMessage": "NOTE: Each page of a PDF file will be created as a single Scribing question with every question taking on the same question details. You can choose to leave the optional inputs blank and return to edit the questions again after creation." + "course.assessment.question.rubricBasedResponses.categoryName": { + "defaultMessage": "Category Name" }, - "course.assessment.question.scribing.ScribingQuestionForm.skillsFieldLabel": { - "defaultMessage": "Skills", - "description": "Label for the skills input field." + "course.assessment.question.rubricBasedResponses.enableAiGrading": { + "defaultMessage": "Enable AI to auto-grade submissions" }, - "course.assessment.question.scribing.ScribingQuestionForm.staffOnlyCommentsFieldLabel": { - "defaultMessage": "Staff only comments", - "description": "Label for the staff only comments input field." + "course.assessment.question.rubricBasedResponses.enableAiGradingDescription": { + "defaultMessage": "AI will assign rubric scores and draft feedback for you to review and publish." }, - "course.assessment.question.scribing.ScribingQuestionForm.submitButton": { - "defaultMessage": "Submit", - "description": "Button for submitting the form." + "course.assessment.question.rubricBasedResponses.rubric": { + "defaultMessage": "Rubric" }, - "course.assessment.question.scribing.ScribingQuestionForm.submitFailureMessage": { - "defaultMessage": "An error occurred, please try again." + "course.assessment.question.rubricBasedResponses.rubricHint": { + "defaultMessage": "Rubric is used to grade the student's submission." }, - "course.assessment.question.scribing.ScribingQuestionForm.submittingMessage": { - "defaultMessage": "Submitting...", - "description": "Text to be displayed when waiting for server response after form submission." + "course.assessment.question.rubricPlayground.addAnswersPromptAction": { + "defaultMessage": "Add" }, - "course.assessment.question.scribing.ScribingQuestionForm.titleFieldLabel": { - "defaultMessage": "Title", - "description": "Label for the title input field." + "course.assessment.question.rubricPlayground.addAnswersTitle": { + "defaultMessage": "Add Sample Answers" }, - "course.assessment.question.scribing.ScribingQuestionForm.valueMoreThan1000Error": { - "defaultMessage": "Value must be less than 1000." + "course.assessment.question.rubricPlayground.addExistingAnswers": { + "defaultMessage": "Add existing answers" }, - "course.assessment.question.textResponses.addSolution": { - "defaultMessage": "Add a new solution" + "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { + "defaultMessage": "Add {inputComponent} random student answer(s)" }, - "course.assessment.question.textResponses.allowFileUpload": { - "defaultMessage": "Allow file upload in the answer" + "course.assessment.question.rubricPlayground.addSampleAnswers": { + "defaultMessage": "Add Sample Answers" }, - "course.assessment.question.textResponses.deleteSolution": { - "defaultMessage": "Delete solution" + "course.assessment.question.rubricPlayground.answer": { + "defaultMessage": "Answer" }, - "course.assessment.question.textResponses.exactMatch": { - "defaultMessage": "Exact Match" + "course.assessment.question.rubricPlayground.apply": { + "defaultMessage": "Apply" }, - "course.assessment.question.textResponses.fileUploadNote": { - "defaultMessage": "Note: File upload question is not auto-gradable. The autograder will always award the maximum grade." + "course.assessment.question.rubricPlayground.applyFailure": { + "defaultMessage": "Failed to apply grading results" }, - "course.assessment.question.textResponses.noSolutionsNote": { - "defaultMessage": "If no solutions are provided, the autograder will always award the maximum grade." + "course.assessment.question.rubricPlayground.applySuccess": { + "defaultMessage": "Grading rubric, prompt, and results successfully applied." }, - "course.assessment.question.textResponses.atLeastOneSolutionNote": { - "defaultMessage": "If at least one solution is provided, students will only be able to input plain text." + "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { + "defaultMessage": "Applying this rubric will assign grades to all student answers, including the ones not yet evaluated on this page." }, - "course.assessment.question.textResponses.exactMatchSolutionNote": { - "defaultMessage": "If Exact Match is selected, solutions with multiple lines must match student answers exactly for the answer to be graded as correct. If such a match is found, the autograder will award the specified grade without considering other solutions." + "course.assessment.question.rubricPlayground.applyingRubricGradingData": { + "defaultMessage": "Applying rubric grading data..." }, - "course.assessment.question.textResponses.solutionGradesExceedMaximumGradeNote": { - "defaultMessage": "If the student answer satisfies multiple solutions, the autograder will award the sum of those solutions' grades, up to the question's maximum grade." + "course.assessment.question.rubricPlayground.categoryHeading": { + "defaultMessage": "C{index}" }, - "course.assessment.question.textResponses.grade": { - "defaultMessage": "Grade" + "course.assessment.question.rubricPlayground.compare": { + "defaultMessage": "Compare" }, - "course.assessment.question.textResponses.keyword": { - "defaultMessage": "Keyword" + "course.assessment.question.rubricPlayground.comparingRevisions": { + "defaultMessage": "Comparing {count} revisions" }, - "course.assessment.question.textResponses.newSolutionCannotUndo": { - "defaultMessage": "This is a new solution. It will immediately disappear if you delete before saving it." + "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { + "defaultMessage": "Confirm AI Grading Application" }, - "course.assessment.question.textResponses.solution": { - "defaultMessage": "Solution" + "course.assessment.question.rubricPlayground.confirmProceed": { + "defaultMessage": "Are you sure you wish to proceed?" }, - "course.assessment.question.textResponses.solutionType": { - "defaultMessage": "Type of Solution" + "course.assessment.question.rubricPlayground.dismiss": { + "defaultMessage": "Dismiss" }, - "course.assessment.question.textResponses.solutionWillBeDeleted": { - "defaultMessage": "This solution will be deleted once you save your changes." + "course.assessment.question.rubricPlayground.evaluate": { + "defaultMessage": "Evaluate" }, - "course.assessment.question.textResponses.solutions": { - "defaultMessage": "Solutions" + "course.assessment.question.rubricPlayground.evaluateAll": { + "defaultMessage": "Evaluate All ({count})" }, - "course.assessment.question.textResponses.solutionsHint": { - "defaultMessage": "Adding solutions allows the answer to be autograded." + "course.assessment.question.rubricPlayground.evaluateRemaining": { + "defaultMessage": "Evaluate Remaining ({count})" }, - "course.assessment.question.textResponses.undoDeleteSolution": { - "defaultMessage": "Undo delete solution" + "course.assessment.question.rubricPlayground.evaluating": { + "defaultMessage": "Evaluating" }, - "course.assessment.question.textResponses.zeroGrade": { - "defaultMessage": "0.0" + "course.assessment.question.rubricPlayground.feedback": { + "defaultMessage": "Feedback" }, - "course.assessment.question.textResponses.templateText": { - "defaultMessage": "Template" + "course.assessment.question.rubricPlayground.gradingCategories": { + "defaultMessage": "Grading Categories" }, - "course.assessment.question.textResponses.templateTextDescription": { - "defaultMessage": "Text that appears in the answer area when students attempt this question for the first time." + "course.assessment.question.rubricPlayground.gradingPrompt": { + "defaultMessage": "Grading Prompt" }, - "course.assessment.question.textResponses.regex": { - "defaultMessage": "Regular Expression" + "course.assessment.question.rubricPlayground.gradingPromptDescription": { + "defaultMessage": "Instructions to guide the AI in grading and giving feedback." }, - "course.assessment.question.textResponses.invalidRegex": { - "defaultMessage": "Invalid regular expression" + "course.assessment.question.rubricPlayground.modelAnswer": { + "defaultMessage": "Model Answer" }, - "course.assessment.question.textResponses.spreadsheetFormula": { - "defaultMessage": "Spreadsheet Formula" + "course.assessment.question.rubricPlayground.modelAnswerDescription": { + "defaultMessage": "An example that scores the maximum for each category." }, - "course.assessment.question.textResponses.testSpreadsheet": { - "defaultMessage": "Test spreadsheet" + "course.assessment.question.rubricPlayground.noAnswers": { + "defaultMessage": "No sample answers have been added. Add some to get started." }, - "course.assessment.question.textResponses.testSpreadsheetDescription": { - "defaultMessage": "A test spreadsheet is required to auto grade the student answer." + "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { + "defaultMessage": "You have selected to apply a rubric which is not the latest revision saved on this page." }, - "course.assessment.question.textResponses.testSpreadsheetRequired": { - "defaultMessage": "Please upload a test spreadsheet file." + "course.assessment.question.rubricPlayground.questionGrade": { + "defaultMessage": "Grade" }, - "course.assessment.question.textResponses.spreadsheetAdvancedOptions": { - "defaultMessage": "Advanced Options" + "course.assessment.question.rubricPlayground.reevaluate": { + "defaultMessage": "Re-evaluate" }, - "course.assessment.question.textResponses.spreadsheetRandomization": { - "defaultMessage": "Enable randomization" + "course.assessment.question.rubricPlayground.reevaluateAll": { + "defaultMessage": "Re-evaluate All ({count})" }, - "course.assessment.question.textResponses.spreadsheetRandomizationDescription": { - "defaultMessage": "If enabled, some spreadsheet values will be randomly adjusted when autograding. This is useful to ensure the formula actually computes the right answer instead of a preset value." + "course.assessment.question.rubricPlayground.rubricPlayground": { + "defaultMessage": "Rubric Playground" }, - "course.assessment.question.textResponses.numberOfRandomTests": { - "defaultMessage": "Number of random tests" + "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { + "defaultMessage": "Sample Answer Evaluations" + }, + "course.assessment.question.rubricPlayground.savedRubric": { + "defaultMessage": "Saved Rubric, {date}" + }, + "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { + "defaultMessage": "Search answers by student name or grade" + }, + "course.assessment.question.rubricPlayground.student": { + "defaultMessage": "Student" + }, + "course.assessment.question.rubricPlayground.totalGrade": { + "defaultMessage": "Total" + }, + "course.assessment.question.rubricPlayground.viewEditRubric": { + "defaultMessage": "View / Edit Rubric" + }, + "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { + "defaultMessage": "Write the answer here" + }, + "course.assessment.question.rubricPlayground.writeCustomAnswer": { + "defaultMessage": "Write a custom answer" + }, + "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { + "defaultMessage": "Cannot be blank." + }, + "course.assessment.question.scribing.ScribingQuestionForm.chooseFileButton": { + "defaultMessage": "Choose File", + "description": "Button for adding an image attachment." + }, + "course.assessment.question.scribing.ScribingQuestionForm.descriptionFieldLabel": { + "defaultMessage": "Description", + "description": "Label for the description input field." + }, + "course.assessment.question.scribing.ScribingQuestionForm.fetchFailureMessage": { + "defaultMessage": "An error occurred, please try again." + }, + "course.assessment.question.scribing.ScribingQuestionForm.fileAttachmentRequired": { + "defaultMessage": "File attachment required." + }, + "course.assessment.question.scribing.ScribingQuestionForm.fileUploaded": { + "defaultMessage": "File uploaded:" + }, + "course.assessment.question.scribing.ScribingQuestionForm.lessThanEqualZeroValidationError": { + "defaultMessage": "Value must be greater than 0." + }, + "course.assessment.question.scribing.ScribingQuestionForm.maximumGradeFieldLabel": { + "defaultMessage": "Maximum Grade", + "description": "Label for the maximum grade input field." + }, + "course.assessment.question.scribing.ScribingQuestionForm.noFileChosenMessage": { + "defaultMessage": "No file chosen", + "description": "Message to be displayed when no file is chosen for a file input." + }, + "course.assessment.question.scribing.ScribingQuestionForm.positiveNumberValidationError": { + "defaultMessage": "Value must be positive." + }, + "course.assessment.question.scribing.ScribingQuestionForm.resolveErrorsMessage": { + "defaultMessage": "This form has errors, please resolve before submitting." + }, + "course.assessment.question.scribing.ScribingQuestionForm.scribingQuestionWarning": { + "defaultMessage": "NOTE: Each page of a PDF file will be created as a single Scribing question with every question taking on the same question details. You can choose to leave the optional inputs blank and return to edit the questions again after creation." + }, + "course.assessment.question.scribing.ScribingQuestionForm.skillsFieldLabel": { + "defaultMessage": "Skills", + "description": "Label for the skills input field." + }, + "course.assessment.question.scribing.ScribingQuestionForm.staffOnlyCommentsFieldLabel": { + "defaultMessage": "Staff only comments", + "description": "Label for the staff only comments input field." + }, + "course.assessment.question.scribing.ScribingQuestionForm.submitButton": { + "defaultMessage": "Submit", + "description": "Button for submitting the form." + }, + "course.assessment.question.scribing.ScribingQuestionForm.submitFailureMessage": { + "defaultMessage": "An error occurred, please try again." + }, + "course.assessment.question.scribing.ScribingQuestionForm.submittingMessage": { + "defaultMessage": "Submitting...", + "description": "Text to be displayed when waiting for server response after form submission." + }, + "course.assessment.question.scribing.ScribingQuestionForm.titleFieldLabel": { + "defaultMessage": "Title", + "description": "Label for the title input field." + }, + "course.assessment.question.scribing.ScribingQuestionForm.valueMoreThan1000Error": { + "defaultMessage": "Value must be less than 1000." }, "course.assessment.question.textResponses.SpreadsheetManager.fixedRandomSeed": { "defaultMessage": "Fixed random seed" @@ -2342,24 +2879,81 @@ "course.assessment.question.textResponses.SpreadsheetManager.fixedTimestampDescription": { "defaultMessage": "If checked, formulas that use the current date/time (NOW, TODAY, etc.) will use this specific date/time instead." }, - "course.assessment.question.textResponses.randomizationMode": { - "defaultMessage": "Autograding Value Configuration" + "course.assessment.question.textResponses.addSolution": { + "defaultMessage": "Add a new solution" }, - "course.assessment.question.textResponses.randomizationModeDescription": { - "defaultMessage": "Click on any cell to configure how its value should be replaced/randomized during autograding." + "course.assessment.question.textResponses.atLeastOneSolutionNote": { + "defaultMessage": "If at least one solution is provided, students will only be able to input plain text." }, - "course.assessment.question.textResponses.randomizationModeClearAll": { - "defaultMessage": "Clear All" + "course.assessment.question.textResponses.attachmentSettingRequired": { + "defaultMessage": "Attachment Setting should be defined in this question" }, - "course.assessment.question.textResponses.randomizationModeRestoreDefaults": { - "defaultMessage": "Restore Defaults" + "course.assessment.question.textResponses.attachmentSettings": { + "defaultMessage": "Attachment Settings" }, - "course.assessment.question.textResponses.randomizationModePopoverTitle": { - "defaultMessage": "During autograding..." + "course.assessment.question.textResponses.attachmentSettingsDescription": { + "defaultMessage": "When students are attempting this question," + }, + "course.assessment.question.textResponses.dateRandomizationMode": { + "defaultMessage": "Use random date value" + }, + "course.assessment.question.textResponses.deleteSolution": { + "defaultMessage": "Delete solution" + }, + "course.assessment.question.textResponses.exactMatch": { + "defaultMessage": "Exact Match" + }, + "course.assessment.question.textResponses.exactMatchSolutionNote": { + "defaultMessage": "If Exact Match is selected, solutions with multiple lines must match student answers exactly for the answer to be graded as correct. If such a match is found, the autograder will award the specified grade without considering other solutions." + }, + "course.assessment.question.textResponses.fileUploadNote": { + "defaultMessage": "Note: File upload question is not auto-gradable. The autograder will always award the maximum grade." + }, + "course.assessment.question.textResponses.grade": { + "defaultMessage": "Grade" + }, + "course.assessment.question.textResponses.invalidRegex": { + "defaultMessage": "Invalid regular expression" + }, + "course.assessment.question.textResponses.isAttachmentRequired": { + "defaultMessage": "Require file upload for this question" + }, + "course.assessment.question.textResponses.keyword": { + "defaultMessage": "Keyword" + }, + "course.assessment.question.textResponses.maxAttachmentSize": { + "defaultMessage": "Max Size per Attachment" + }, + "course.assessment.question.textResponses.maxAttachments": { + "defaultMessage": "Max Number of Attachments" + }, + "course.assessment.question.textResponses.multipleAttachments": { + "defaultMessage": "Multiple Attachments" + }, + "course.assessment.question.textResponses.multipleFileAttachmentDescription": { + "defaultMessage": "They can upload several attachments." + }, + "course.assessment.question.textResponses.newSolutionCannotUndo": { + "defaultMessage": "This is a new solution. It will immediately disappear if you delete before saving it." + }, + "course.assessment.question.textResponses.noAttachment": { + "defaultMessage": "No Attachment" + }, + "course.assessment.question.textResponses.noAttachmentDescription": { + "defaultMessage": "They will not be able to upload any attachment." }, "course.assessment.question.textResponses.noRandomizationMode": { "defaultMessage": "Keep original value" }, + "course.assessment.question.textResponses.noSolutionsNote": { + "defaultMessage": "If no solutions are provided, the autograder will always award the maximum grade." + }, + "course.assessment.question.textResponses.numberOfRandomTests": { + "defaultMessage": "Number of random tests" + }, + "course.assessment.question.textResponses.numericRandomizationMode": { + "defaultMessage": "Use random numeric value" + }, "course.assessment.question.textResponses.overrideRandomizationMode": { "defaultMessage": "Override with specified value" }, @@ -2369,17 +2963,20 @@ "course.assessment.question.textResponses.overrideValue": { "defaultMessage": "Value" }, - "course.assessment.question.textResponses.numericRandomizationMode": { - "defaultMessage": "Use random numeric value" + "course.assessment.question.textResponses.randomizationMode": { + "defaultMessage": "Autograding Value Configuration" }, - "course.assessment.question.textResponses.roundToInteger": { - "defaultMessage": "Round down to nearest integer" + "course.assessment.question.textResponses.randomizationModeClearAll": { + "defaultMessage": "Clear All" }, - "course.assessment.question.textResponses.stringRandomizationMode": { - "defaultMessage": "Randomize characters in value" + "course.assessment.question.textResponses.randomizationModeDescription": { + "defaultMessage": "Click on any cell to configure how its value should be replaced/randomized during autograding." }, - "course.assessment.question.textResponses.stringRandomizationModeDescription": { - "defaultMessage": "Each character class to be randomized may be replaced with different character(s) from the same class." + "course.assessment.question.textResponses.randomizationModePopoverTitle": { + "defaultMessage": "During autograding..." + }, + "course.assessment.question.textResponses.randomizationModeRestoreDefaults": { + "defaultMessage": "Restore Defaults" }, "course.assessment.question.textResponses.randomizeDigits": { "defaultMessage": "Randomize digits (0-9)" @@ -2387,152 +2984,86 @@ "course.assessment.question.textResponses.randomizeLetters": { "defaultMessage": "Randomize letters (a-z)" }, - "course.assessment.question.textResponses.dateRandomizationMode": { - "defaultMessage": "Use random date value" + "course.assessment.question.textResponses.regex": { + "defaultMessage": "Regular Expression" }, "course.assessment.question.textResponses.roundToDay": { "defaultMessage": "Round down to day start (00:00)" }, + "course.assessment.question.textResponses.roundToInteger": { + "defaultMessage": "Round down to nearest integer" + }, "course.assessment.question.textResponses.shuffleRandomizationMode": { "defaultMessage": "Shuffle value with other cells" }, "course.assessment.question.textResponses.shuffleRandomizationModeDescription": { "defaultMessage": "The values of each cell marked \"Shuffle\" will be randomly rearranged. The overall distribution remains the same, but each value may end up in a different cell." }, - "course.assessment.question.rubricPlayground.rubricPlayground": { - "defaultMessage": "Rubric Playground" + "course.assessment.question.textResponses.singleFileAttachment": { + "defaultMessage": "Single Attachment" }, - "course.assessment.question.rubricPlayground.savedRubric": { - "defaultMessage": "Saved Rubric, {date}" + "course.assessment.question.textResponses.singleFileAttachmentDescription": { + "defaultMessage": "They can only upload one attachment." }, - "course.assessment.question.rubricPlayground.viewEditRubric": { - "defaultMessage": "View / Edit Rubric" + "course.assessment.question.textResponses.solution": { + "defaultMessage": "Solution" }, - "course.assessment.question.rubricPlayground.evaluate": { - "defaultMessage": "Evaluate" + "course.assessment.question.textResponses.solutionGradesExceedMaximumGradeNote": { + "defaultMessage": "If the student answer satisfies multiple solutions, the autograder will award the sum of those solutions' grades, up to the question's maximum grade." }, - "course.assessment.question.rubricPlayground.compare": { - "defaultMessage": "Compare" + "course.assessment.question.textResponses.solutionType": { + "defaultMessage": "Type of Solution" }, - "course.assessment.question.rubricPlayground.apply": { - "defaultMessage": "Apply" + "course.assessment.question.textResponses.solutionWillBeDeleted": { + "defaultMessage": "This solution will be deleted once you save your changes." }, - "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { - "defaultMessage": "Confirm AI Grading Application" + "course.assessment.question.textResponses.solutions": { + "defaultMessage": "Solutions" }, - "course.assessment.question.rubricPlayground.applyingRubricGradingData": { - "defaultMessage": "Applying rubric grading data..." + "course.assessment.question.textResponses.solutionsHint": { + "defaultMessage": "Adding solutions allows the answer to be autograded." }, - "course.assessment.question.rubricPlayground.applySuccess": { - "defaultMessage": "Grading rubric, prompt, and results successfully applied." + "course.assessment.question.textResponses.spreadsheetAdvancedOptions": { + "defaultMessage": "Advanced Options" }, - "course.assessment.question.rubricPlayground.applyFailure": { - "defaultMessage": "Failed to apply grading results" - }, - "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { - "defaultMessage": "You have selected to apply a rubric which is not the latest revision saved on this page." - }, - "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { - "defaultMessage": "Applying this rubric will assign grades to all student answers, including the ones not yet evaluated on this page." - }, - "course.assessment.question.rubricPlayground.confirmProceed": { - "defaultMessage": "Are you sure you wish to proceed?" - }, - "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { - "defaultMessage": "Sample Answer Evaluations" - }, - "course.assessment.question.rubricPlayground.addSampleAnswers": { - "defaultMessage": "Add Sample Answers" - }, - "course.assessment.question.rubricPlayground.evaluateAll": { - "defaultMessage": "Evaluate All ({count})" - }, - "course.assessment.question.rubricPlayground.reevaluateAll": { - "defaultMessage": "Re-evaluate All ({count})" - }, - "course.assessment.question.rubricPlayground.evaluateRemaining": { - "defaultMessage": "Evaluate Remaining ({count})" - }, - "course.assessment.question.rubricPlayground.comparingRevisions": { - "defaultMessage": "Comparing {count} revisions" - }, - "course.assessment.question.rubricPlayground.addSampleAnswersTitle": { - "defaultMessage": "Add Sample Answers" - }, - "course.assessment.question.rubricPlayground.add": { - "defaultMessage": "Add" - }, - "course.assessment.question.rubricPlayground.addExistingAnswers": { - "defaultMessage": "Add existing answers" - }, - "course.assessment.question.rubricPlayground.student": { - "defaultMessage": "Student" - }, - "course.assessment.question.rubricPlayground.questionGrade": { - "defaultMessage": "Grade" - }, - "course.assessment.question.rubricPlayground.categoryHeading": { - "defaultMessage": "C{index}" - }, - "course.assessment.question.rubricPlayground.answer": { - "defaultMessage": "Answer" - }, - "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { - "defaultMessage": "Search answers by student name or grade" - }, - "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { - "defaultMessage": "Add {inputComponent} random student answer(s)" - }, - "course.assessment.question.rubricPlayground.writeCustomAnswer": { - "defaultMessage": "Write a custom answer" - }, - "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { - "defaultMessage": "Write the answer here" - }, - "course.assessment.question.rubricPlayground.dismiss": { - "defaultMessage": "Dismiss" - }, - "course.assessment.question.rubricPlayground.noAnswers": { - "defaultMessage": "No sample answers have been added. Add some to get started." - }, - "course.assessment.question.rubricPlayground.reevaluate": { - "defaultMessage": "Re-evaluate" + "course.assessment.question.textResponses.spreadsheetFormula": { + "defaultMessage": "Spreadsheet Formula" }, - "course.assessment.question.rubricPlayground.totalGrade": { - "defaultMessage": "Total" + "course.assessment.question.textResponses.spreadsheetRandomization": { + "defaultMessage": "Enable randomization" }, - "course.assessment.question.rubricPlayground.feedback": { - "defaultMessage": "Feedback" + "course.assessment.question.textResponses.spreadsheetRandomizationDescription": { + "defaultMessage": "If enabled, some spreadsheet values will be randomly adjusted when autograding. This is useful to ensure the formula actually computes the right answer instead of a preset value." }, - "course.assessment.question.rubricPlayground.evaluating": { - "defaultMessage": "Evaluating" + "course.assessment.question.textResponses.stringRandomizationMode": { + "defaultMessage": "Randomize characters in value" }, - "course.assessment.question.rubricPlayground.gradingPrompt": { - "defaultMessage": "Grading Prompt" + "course.assessment.question.textResponses.stringRandomizationModeDescription": { + "defaultMessage": "Each character class to be randomized may be replaced with different character(s) from the same class." }, - "course.assessment.question.rubricPlayground.gradingPromptDescription": { - "defaultMessage": "Instructions to guide the AI in grading and giving feedback." + "course.assessment.question.textResponses.templateText": { + "defaultMessage": "Template" }, - "course.assessment.question.rubricPlayground.modelAnswer": { - "defaultMessage": "Model Answer" + "course.assessment.question.textResponses.templateTextDescription": { + "defaultMessage": "Text that appears in the answer area when students attempt this question for the first time." }, - "course.assessment.question.rubricPlayground.modelAnswerDescription": { - "defaultMessage": "An example that scores the maximum for each category." + "course.assessment.question.textResponses.testSpreadsheet": { + "defaultMessage": "Test spreadsheet" }, - "course.assessment.question.rubricPlayground.gradingCategories": { - "defaultMessage": "Grading Categories" + "course.assessment.question.textResponses.testSpreadsheetDescription": { + "defaultMessage": "A test spreadsheet is required to auto grade the student answer." }, - "course.assessment.question.rubricPlayground.addNewCategory": { - "defaultMessage": "Add New Category" + "course.assessment.question.textResponses.testSpreadsheetRequired": { + "defaultMessage": "Please upload a test spreadsheet file." }, - "course.assessment.question.rubricPlayground.categoryName": { - "defaultMessage": "Category Name" + "course.assessment.question.textResponses.undoDeleteSolution": { + "defaultMessage": "Undo delete solution" }, - "course.assessment.question.rubricPlayground.max": { - "defaultMessage": "Max" + "course.assessment.question.textResponses.validAttachmentSettingValues": { + "defaultMessage": "Attachment Settings should be either no attachment, single file attachment, or multiple file attachment" }, - "course.assessment.question.rubricPlayground.addNewGrade": { - "defaultMessage": "Add New Grade" + "course.assessment.question.textResponses.zeroGrade": { + "defaultMessage": "0.0" }, "course.assessment.session.assessmentNotStarted": { "defaultMessage": "The assessment has not started yet. Please come back after {startDate}." @@ -2558,9 +3089,6 @@ "course.assessment.show.assessmentOnlyAvailableFrom": { "defaultMessage": "This assessment will only be available from" }, - "course.assessment.show.audioResponse": { - "defaultMessage": "Audio Response" - }, "course.assessment.show.baseExp": { "defaultMessage": "Base EXP" }, @@ -2594,6 +3122,9 @@ "course.assessment.show.chooseAssessmentToDuplicateInto": { "defaultMessage": "Choose an assessment to duplicate into" }, + "course.assessment.show.comprehension": { + "defaultMessage": "Comprehension" + }, "course.assessment.show.delete": { "defaultMessage": "Delete" }, @@ -2651,9 +3182,15 @@ "course.assessment.show.errorMovingQuestion": { "defaultMessage": "An error occurred while moving the question." }, + "course.assessment.show.failedSyncingWithKoditsu": { + "defaultMessage": "Not Synced with Koditsu" + }, "course.assessment.show.fileUpload": { "defaultMessage": "File Upload" }, + "course.assessment.show.fileUploadDescription": { + "defaultMessage": "Settings for the number of attachments allowed (none, one, or multiple)" + }, "course.assessment.show.files": { "defaultMessage": "Files" }, @@ -2666,20 +3203,20 @@ "course.assessment.show.forumPostResponse": { "defaultMessage": "Forum Post Response" }, - "course.assessment.show.gradedTestCases": { - "defaultMessage": "Graded test cases" - }, "course.assessment.show.generate": { "defaultMessage": "Generate Questions" }, - "course.assessment.show.generateTooltip": { - "defaultMessage": "Collaborate with Codaveri AI to create questions" + "course.assessment.show.generateFromProgrammingQuestion": { + "defaultMessage": "Generate a similar question with Codaveri AI" }, "course.assessment.show.generateFromQuestion": { "defaultMessage": "Generate a similar question with AI" }, - "course.assessment.show.generateFromProgrammingQuestion": { - "defaultMessage": "Generate a similar question with Codaveri AI" + "course.assessment.show.generateTooltip": { + "defaultMessage": "Collaborate with Codaveri AI to create questions" + }, + "course.assessment.show.gradedTestCases": { + "defaultMessage": "Graded test cases" }, "course.assessment.show.gradingMode": { "defaultMessage": "Grading mode" @@ -2696,6 +3233,9 @@ "course.assessment.show.hideOptions": { "defaultMessage": "Hide options" }, + "course.assessment.show.koditsuMode": { + "defaultMessage": "Koditsu" + }, "course.assessment.show.manageComponents": { "defaultMessage": "Manage Components in Course Settings" }, @@ -2744,6 +3284,9 @@ "course.assessment.show.newQuestion": { "defaultMessage": "New Question" }, + "course.assessment.show.newRubricBasedResponse": { + "defaultMessage": "New Rubric Based Response Question" + }, "course.assessment.show.newScribing": { "defaultMessage": "New Scribing Question" }, @@ -2801,6 +3344,9 @@ "course.assessment.show.requirementsHint": { "defaultMessage": "The following items must be fulfilled to unlock this assessment." }, + "course.assessment.show.rubricBasedResponse": { + "defaultMessage": "Rubric-Based Response" + }, "course.assessment.show.scribing": { "defaultMessage": "Scribing" }, @@ -2810,15 +3356,15 @@ "course.assessment.show.showMcqMrqSolution": { "defaultMessage": "Show MCQ/MRQ solutions" }, - "course.assessment.show.showRubricToStudents": { - "defaultMessage": "Show rubric breakdown to students" - }, "course.assessment.show.showMcqSubmitResult": { "defaultMessage": "Show MCQ submit result" }, "course.assessment.show.showOptions": { "defaultMessage": "Show options" }, + "course.assessment.show.showRubricToStudents": { + "defaultMessage": "Show rubric breakdown to students" + }, "course.assessment.show.sureChangingQuestionType": { "defaultMessage": "Sure you're changing this question type?" }, @@ -2828,6 +3374,12 @@ "course.assessment.show.sureDeletingQuestion": { "defaultMessage": "Sure you're deleting this question?" }, + "course.assessment.show.syncedWithKoditsu": { + "defaultMessage": "Synced with Koditsu" + }, + "course.assessment.show.syncingWithKoditsu": { + "defaultMessage": "Syncing with Koditsu" + }, "course.assessment.show.textResponse": { "defaultMessage": "Text Response" }, @@ -2843,6 +3395,9 @@ "course.assessment.show.unsubmittingAndChangingQuestionType": { "defaultMessage": "Unsubmitting submissions and changing your question type..." }, + "course.assessment.show.voiceResponse": { + "defaultMessage": "Audio Response" + }, "course.assessment.show.whileHoldingToCancelMoving": { "defaultMessage": "while holding to cancel moving." }, @@ -2945,119 +3500,113 @@ "course.assessment.skills.SkillsTable.uncategorised": { "defaultMessage": "Uncategorised Skills" }, - "course.assessment.liveFeedback.questionTitle": { - "defaultMessage": "Question {index}" + "course.assessment.statistics.ancestorFail": { + "defaultMessage": "Failed to fetch past iterations of this assessment." }, - "course.assessment.liveFeedback.messageTimingTitle": { - "defaultMessage": "Generated at: {usedAt}" + "course.assessment.statistics.ancestorSelect.current": { + "defaultMessage": "Current" }, - "course.assessment.liveFeedback.liveFeedbackName": { - "defaultMessage": "Get Help" + "course.assessment.statistics.ancestorSelect.fromCourse": { + "defaultMessage": "From {courseTitle}" }, - "course.assessment.liveFeedback.comments": { - "defaultMessage": "Comments" + "course.assessment.statistics.ancestorSelect.subtitle": { + "defaultMessage": "Compare against past versions of this assessment:" }, - "course.assessment.liveFeedback.lineHeader": { - "defaultMessage": "Line {lineNumber}" + "course.assessment.statistics.ancestorSelect.title": { + "defaultMessage": "Duplication History" }, - "course.assessment.submission.GetHelpChatPage.chatInputText": { - "defaultMessage": "How can we help you?" + "course.assessment.statistics.ancestorStatisticsFail": { + "defaultMessage": "Failed to fetch ancestor's statistics." }, - "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { - "defaultMessage": "{numMessages} / {maxMessages} {numMessages, plural, one {message} other {messages}} remaining" + "course.assessment.statistics.answers": { + "defaultMessage": "Answers" }, - "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { - "defaultMessage": "You have reached the message limit for this question." + "course.assessment.statistics.attemptCount": { + "defaultMessage": "Attempt Count" }, - "course.assessment.submission.GetHelpChatPage.codeUpdated": { - "defaultMessage": "Code Updated" + "course.assessment.statistics.attempts.filename": { + "defaultMessage": "Question-level Attempt Statistics for {assessment}" }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { - "defaultMessage": "Line {lineNumber}" + "course.assessment.statistics.attempts.greenCellLegend": { + "defaultMessage": "Correct" }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { - "defaultMessage": "{filename}:{lineNumber}" + "course.assessment.statistics.attempts.redCellLegend": { + "defaultMessage": "Incorrect" }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { - "defaultMessage": "The chat above has ended. Start a new chat?" + "course.assessment.statistics.closePrompt": { + "defaultMessage": "Close" }, - "course.assessment.plagiarism.plagiarism": { - "defaultMessage": "Plagiarism Results" + "course.assessment.statistics.comments": { + "defaultMessage": "Comments" }, - "course.assessment.plagiarism.status": { - "defaultMessage": "Plagiarism Check Status" + "course.assessment.statistics.duplicationHistory": { + "defaultMessage": "Duplication History" }, - "course.assessment.plagiarism.lastRunTime": { - "defaultMessage": "Last run at: {date}" + "course.assessment.statistics.email": { + "defaultMessage": "Email" }, - "course.assessment.plagiarism.start": { - "defaultMessage": "New Plagiarism Check" + "course.assessment.statistics.fail": { + "defaultMessage": "Failed to fetch statistics." }, - "course.assessment.plagiarism.notStarted": { - "defaultMessage": "No plagiarism check has been run" + "course.assessment.statistics.gradeDisplay": { + "defaultMessage": "Grade: {grade} / {maxGrade}" }, - "course.assessment.plagiarism.confirmStartTitle": { - "defaultMessage": "Confirm Plagiarism Check?" + "course.assessment.statistics.gradeDistribution": { + "defaultMessage": "Grade Distribution" }, - "course.assessment.plagiarism.confirmStartMessage": { - "defaultMessage": "Running a new plagiarism check will remove the previous results." + "course.assessment.statistics.gradeDistribution.datasetLabel": { + "defaultMessage": "Distribution" }, - "course.assessment.plagiarism.results": { - "defaultMessage": "Plagiarism Results (similarity between submissions)" + "course.assessment.statistics.gradeDistribution.xAxisLabel": { + "defaultMessage": "Grades" }, - "course.assessment.plagiarism.baseSubmission": { - "defaultMessage": "Base Submission" + "course.assessment.statistics.gradeDistribution.yAxisLabel": { + "defaultMessage": "Submissions" }, - "course.assessment.plagiarism.comparedSubmission": { - "defaultMessage": "Compared Submission" + "course.assessment.statistics.grader": { + "defaultMessage": "Grader" }, - "course.assessment.plagiarism.similarityScore": { - "defaultMessage": "Similarity Score" + "course.assessment.statistics.gradesPerQuestion": { + "defaultMessage": "Grades Per Question" }, - "course.assessment.plagiarism.actions": { - "defaultMessage": "Actions" + "course.assessment.statistics.grayCellLegend": { + "defaultMessage": "Undecided (question is Non-autogradable)" }, - "course.assessment.plagiarism.viewReport": { - "defaultMessage": "View Report" + "course.assessment.statistics.group": { + "defaultMessage": "Group" }, - "course.assessment.plagiarism.downloadPdf": { - "defaultMessage": "Download PDF" + "course.assessment.statistics.header": { + "defaultMessage": "Statistics for {title}" }, - "course.assessment.plagiarism.searchByStudentName": { - "defaultMessage": "Search by Student Name" + "course.assessment.statistics.includePhantom": { + "defaultMessage": "Include Phantom Student" }, - "course.assessment.plagiarism.showSelfPlagiarism": { - "defaultMessage": "Include self-plagiarism comparisons (same student, different courses)" + "course.assessment.statistics.legendHigherLabelGrade": { + "defaultMessage": "Higher Grade" }, - "course.assessment.statistics.answers": { - "defaultMessage": "Answers" + "course.assessment.statistics.legendHigherLabelGradeDiff": { + "defaultMessage": "More Improvement" }, - "course.assessment.statistics.attempts.filename": { - "defaultMessage": "Question-level Attempt Statistics for {assessment}" + "course.assessment.statistics.legendLowerLabelGrade": { + "defaultMessage": "Lower Grade" }, - "course.assessment.statistics.attempts.greenCellLegend": { - "defaultMessage": "Correct" + "course.assessment.statistics.legendLowerLabelGradeDiff": { + "defaultMessage": "Less Improvement" }, - "course.assessment.statistics.attempts.redCellLegend": { - "defaultMessage": "Incorrect" + "course.assessment.statistics.legendLowerLabelMessagesSent": { + "defaultMessage": "Lower Usage" }, - "course.assessment.statistics.closePrompt": { - "defaultMessage": "Close" + "course.assessment.statistics.legendLowerLabelWordCount": { + "defaultMessage": "Lower Word Count" }, - "course.assessment.statistics.grader": { - "defaultMessage": "Grader" - }, - "course.assessment.statistics.grayCellLegend": { - "defaultMessage": "Undecided (question is Non-autogradable)" - }, - "course.assessment.statistics.group": { - "defaultMessage": "Group" - }, - "course.assessment.statistics.legendHigherusage": { + "course.assessment.statistics.legendUpperLabelMessagesSent": { "defaultMessage": "Higher Usage" }, - "course.assessment.statistics.legendLowerUsage": { - "defaultMessage": "Lower Usage" + "course.assessment.statistics.legendUpperLabelWordCount": { + "defaultMessage": "Higher Word Count" + }, + "course.assessment.statistics.liveFeedback": { + "defaultMessage": "Get Help" }, "course.assessment.statistics.liveFeedback.filename": { "defaultMessage": "Question-level Get Help Statistics for {assessment}" @@ -3083,78 +3632,30 @@ "course.assessment.statistics.nameGroupsSearchText": { "defaultMessage": "Search by Name or Groups" }, + "course.assessment.statistics.noIncludePhantom": { + "defaultMessage": "*All statistics in this duplicated assessments does not include Phantom Students" + }, "course.assessment.statistics.noSubmission": { "defaultMessage": "No submission yet" }, "course.assessment.statistics.onlyForAutogradableAssessment": { "defaultMessage": "This table is only displayed for Assessment with at least one Autograded Questions" }, + "course.assessment.statistics.pastAnswerTitle": { + "defaultMessage": "Submitted At: {submittedAt}" + }, "course.assessment.statistics.questionDisplayTitle": { "defaultMessage": "Q{index} for {student}" }, "course.assessment.statistics.questionIndex": { "defaultMessage": "Q{index}" }, - "course.assessment.statistics.total": { - "defaultMessage": "Total" - }, - "course.assessment.statistics.workflowState": { - "defaultMessage": "Status" - }, - "course.assessment.statistics.ancestorFail": { - "defaultMessage": "Failed to fetch past iterations of this assessment." - }, - "course.assessment.statistics.ancestorStatisticsFail": { - "defaultMessage": "Failed to fetch ancestor's statistics." - }, - "course.assessment.statistics.fail": { - "defaultMessage": "Failed to fetch statistics." - }, - "course.assessment.statistics.gradeDistribution": { - "defaultMessage": "Grade Distribution" - }, - "course.assessment.statistics.gradeViolin.datasetLabel": { - "defaultMessage": "Distribution" - }, - "course.assessment.statistics.gradeViolin.xAxisLabel": { - "defaultMessage": "Grades" - }, - "course.assessment.statistics.gradeViolin.yAxisLabel": { - "defaultMessage": "Submissions" - }, - "course.assessment.statistics.ancestorSelect.current": { - "defaultMessage": "Current" - }, - "course.assessment.statistics.ancestorSelect.fromCourse": { - "defaultMessage": "From {courseTitle}" - }, - "course.assessment.statistics.ancestorSelect.subtitle": { - "defaultMessage": "Compare against past versions of this assessment:" - }, - "course.assessment.statistics.ancestorSelect.title": { - "defaultMessage": "Duplication History" - }, - "course.assessment.statistics.attemptCount": { - "defaultMessage": "Attempt Count" - }, - "course.assessment.statistics.duplicationHistory": { - "defaultMessage": "Duplication History" - }, - "course.assessment.statistics.gradesPerQuestion": { - "defaultMessage": "Grades Per Question" - }, - "course.assessment.statistics.includePhantom": { - "defaultMessage": "Include Phantom Student" - }, - "course.assessment.statistics.liveFeedback": { - "defaultMessage": "Get Help" - }, - "course.assessment.statistics.header": { - "defaultMessage": "Statistics for {title}" - }, "course.assessment.statistics.statistics": { "defaultMessage": "Statistics" }, + "course.assessment.statistics.submissionPage": { + "defaultMessage": "Go to Answer Page" + }, "course.assessment.statistics.submissionStatuses": { "defaultMessage": "Submission Statuses" }, @@ -3173,9 +3674,18 @@ "course.assessment.statistics.submissionTimeGradeChart.xAxisLabel.withoutDeadline": { "defaultMessage": "Submission Date" }, + "course.assessment.statistics.total": { + "defaultMessage": "Total" + }, + "course.assessment.statistics.workflowState": { + "defaultMessage": "Status" + }, "course.assessment.submission.Annotations.comment": { "defaultMessage": "Add Comment" }, + "course.assessment.submission.Answer.rendererNotImplemented": { + "defaultMessage": "The display for this question type has not been implemented yet." + }, "course.assessment.submission.CodaveriFeedbackStatus.codaveriFeedbackStatus": { "defaultMessage": "Codaveri Feedback Status" }, @@ -3194,14 +3704,77 @@ "course.assessment.submission.EvaluatorErrorPanel.emailSubject": { "defaultMessage": "[Bug Report] Evaluator Error" }, + "course.assessment.submission.FileInput.exactlyOneFileUploadAllowed": { + "defaultMessage": "*You must upload EXACTLY 1 file for this question" + }, + "course.assessment.submission.FileInput.fileName": { + "defaultMessage": "{index}. {name}" + }, + "course.assessment.submission.FileInput.fileTooLargeErrorMessage": { + "defaultMessage": "The following files have size larger than allowed ({maxAttachmentSize} MB)" + }, + "course.assessment.submission.FileInput.fileUploadErrorTitle": { + "defaultMessage": "Error in Uploading Files" + }, + "course.assessment.submission.FileInput.onlyOneFileUploadAllowed": { + "defaultMessage": "*You can only upload AT MOST {maxAttachments} file for this question" + }, + "course.assessment.submission.FileInput.requiredUploadLimitedNumberOfFiles": { + "defaultMessage": "*You can upload AT LEAST 1 and AT MOST {maxAttachments} files for this question" + }, + "course.assessment.submission.FileInput.tooManyFilesErrorMessage": { + "defaultMessage": "You have attempted to upload {numFiles} files, but ONLY {maxAttachmentsAllowed} {maxAttachmentsAllowed, plural, one {file} other {files}} can be uploaded {numAttachments, plural, =0 {} one {since 1 file has been uploaded before} other {since {numAttachments} files has been uploaded before}}" + }, "course.assessment.submission.FileInput.uploadDisabled": { "defaultMessage": "File upload disabled" }, "course.assessment.submission.FileInput.uploadLabel": { "defaultMessage": "Drag and drop or click to upload files" }, + "course.assessment.submission.GetHelpChatPage": { + "defaultMessage": "Get Help" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { + "defaultMessage": "{filename}:{lineNumber}" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { + "defaultMessage": "Line {lineNumber}" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { + "defaultMessage": "The chat above has ended. Start a new chat?" + }, + "course.assessment.submission.GetHelpChatPage.chatInputText": { + "defaultMessage": "How can we help you?" + }, + "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { + "defaultMessage": "{numMessages} / {maxMessages} {numMessages, plural, one {message} other {messages}} remaining" + }, + "course.assessment.submission.GetHelpChatPage.codeUpdated": { + "defaultMessage": "Code Updated" + }, + "course.assessment.submission.GetHelpChatPage.endOfConversation": { + "defaultMessage": "View code after conversation" + }, + "course.assessment.submission.GetHelpChatPage.failedSyncingWithCodaveri": { + "defaultMessage": "Unavailable" + }, + "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { + "defaultMessage": "You have reached the message limit for this question." + }, + "course.assessment.submission.GetHelpChatPage.syncedWithCodaveri": { + "defaultMessage": "Ready" + }, + "course.assessment.submission.GetHelpChatPage.syncingWithCodaveri": { + "defaultMessage": "Preparing" + }, + "course.assessment.submission.ImportedFileView.delete": { + "defaultMessage": "Delete" + }, "course.assessment.submission.ImportedFileView.deleteConfirmation": { - "defaultMessage": "Are you sure you want to delete this file?" + "defaultMessage": "Are you sure you want to delete \"{fileName}\"?" + }, + "course.assessment.submission.ImportedFileView.deleteTitle": { + "defaultMessage": "Delete File" }, "course.assessment.submission.ImportedFileView.noFiles": { "defaultMessage": "No files uploaded." @@ -3209,17 +3782,44 @@ "course.assessment.submission.ImportedFileView.uploadedFiles": { "defaultMessage": "Uploaded Files:" }, - "course.assessment.submission.Answer.missingAnswer": { - "defaultMessage": "There is no answer submitted for this question - this might be caused by the addition of this question after the submission is submitted." + "course.assessment.submission.ResultView.expected": { + "defaultMessage": "Expected" }, - "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { - "defaultMessage": "No past answers." + "course.assessment.submission.ResultView.output": { + "defaultMessage": "Output" }, - "course.assessment.submission.Answer.rendererNotImplemented": { - "defaultMessage": "The display for this question type has not been implemented yet." + "course.assessment.submission.ResultView.solution": { + "defaultMessage": "Solution" }, - "course.assessment.submission.SubmissionAnswer.viewPastAnswers": { - "defaultMessage": "Past Answers" + "course.assessment.submission.ResultView.solutionHeaders.exactMatch": { + "defaultMessage": "Answer matches exactly {solution}" + }, + "course.assessment.submission.ResultView.solutionHeaders.keyword": { + "defaultMessage": "Answer contains keyword {solution}" + }, + "course.assessment.submission.ResultView.solutionHeaders.regex": { + "defaultMessage": "Answer matches regular expression {solution}" + }, + "course.assessment.submission.ResultView.solutionHeaders.spreadsheetFormula": { + "defaultMessage": "Answer formula equivalent to {solution}" + }, + "course.assessment.submission.ResultView.solutionInvertedHeaders.exactMatch": { + "defaultMessage": "Answer is not exactly {solution}" + }, + "course.assessment.submission.ResultView.solutionInvertedHeaders.keyword": { + "defaultMessage": "Answer does not contain keyword {solution}" + }, + "course.assessment.submission.ResultView.solutionInvertedHeaders.regex": { + "defaultMessage": "Answer does not match regular expression {solution}" + }, + "course.assessment.submission.ResultView.solutionInvertedHeaders.spreadsheetFormula": { + "defaultMessage": "Answer formula not equivalent to {solution}" + }, + "course.assessment.submission.SubmissionEditIndex.TimeLimitBanner.hoursMinutesSeconds": { + "defaultMessage": "{hrs, plural, one {# hour} other {# hours}} {mins, plural, =0 {} one {# minute} other {# minutes}} {secs, plural, =0 {} one {# second} other {# seconds}}" + }, + "course.assessment.submission.SubmissionEditIndex.TimeLimitBanner.minutesSeconds": { + "defaultMessage": "{secs, plural, one {# second} other {# seconds}}" }, "course.assessment.submission.SubmissionsIndex.accessLogs": { "defaultMessage": "Access Logs" @@ -3254,6 +3854,9 @@ "course.assessment.submission.SubmissionsIndex.experiencePoints": { "defaultMessage": "EXP Awarded" }, + "course.assessment.submission.SubmissionsIndex.fetchFromKoditsu": { + "defaultMessage": "Fetch Submissions from Koditsu" + }, "course.assessment.submission.SubmissionsIndex.forceSubmit": { "defaultMessage": "Force Submit Remaining" }, @@ -3266,12 +3869,12 @@ "course.assessment.submission.SubmissionsIndex.includePhantoms": { "defaultMessage": "Include phantom users" }, - "lib.translations.myStudents": { - "defaultMessage": "My Students" - }, "course.assessment.submission.SubmissionsIndex.phantom": { "defaultMessage": "Phantom User" }, + "course.assessment.submission.SubmissionsIndex.publishAutoFeedback": { + "defaultMessage": "Publish Automated Programming Feedback ({count})" + }, "course.assessment.submission.SubmissionsIndex.publishGrades": { "defaultMessage": "Publish Grades" }, @@ -3281,21 +3884,6 @@ "course.assessment.submission.SubmissionsIndex.remind": { "defaultMessage": "Send Reminder Emails" }, - "lib.translations.staff": { - "defaultMessage": "Staff" - }, - "lib.translations.students": { - "defaultMessage": "Students" - }, - "lib.translations.myStudentsIncludingPhantoms": { - "defaultMessage": "My Students (Including Phantoms)" - }, - "lib.translations.studentsIncludingPhantoms": { - "defaultMessage": "Students (Including Phantoms)" - }, - "lib.translations.staffIncludingPhantoms": { - "defaultMessage": "Staff (Including Phantoms)" - }, "course.assessment.submission.SubmissionsIndex.submissionStatus": { "defaultMessage": "Status" }, @@ -3311,6 +3899,9 @@ "course.assessment.submission.SubmissionsIndex.userName": { "defaultMessage": "Name" }, + "course.assessment.submission.TestCaseView.allFailed": { + "defaultMessage": "All failed" + }, "course.assessment.submission.TestCaseView.allPassed": { "defaultMessage": "All passed" }, @@ -3350,8 +3941,17 @@ "course.assessment.submission.TestCaseView.standardOutput": { "defaultMessage": "Standard Output" }, + "course.assessment.submission.TestCaseView.testCasesPassed": { + "defaultMessage": "{numPassed}/{numTestCases} passed" + }, "course.assessment.submission.UploadedFileView.deleteConfirmation": { - "defaultMessage": "Are you sure you want to delete this attachment?" + "defaultMessage": "Are you sure you want to delete {fileName}?" + }, + "course.assessment.submission.UploadedFileView.deleteTitle": { + "defaultMessage": "Delete File" + }, + "course.assessment.submission.UploadedFileView.deleting": { + "defaultMessage": "Delete" }, "course.assessment.submission.UploadedFileView.noFiles": { "defaultMessage": "No files uploaded." @@ -3360,7 +3960,7 @@ "defaultMessage": "Uploaded Files" }, "course.assessment.submission.VoiceResponseAnswer.chooseVoiceFileExplain": { - "defaultMessage": "Drag your audio file here, or click to select an audio file. Only wav and mp3 formats are supported. Alternatively, you may use the recorder below to record your response" + "defaultMessage": "Drag and drop or click to upload your WAV / MP3 files. Alternatively, use the recorder below to record your response" }, "course.assessment.submission.VoiceResponseAnswer.pleaseRecordYourVoice": { "defaultMessage": "Please record your voice" @@ -3485,6 +4085,24 @@ "course.assessment.submission.answerSubmitted": { "defaultMessage": "Answer Submitted" }, + "course.assessment.submission.answerTooLarge": { + "defaultMessage": "Answer Too Large" + }, + "course.assessment.submission.answerTooLargeError": { + "defaultMessage": "Your answer must be less than 2 MB." + }, + "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { + "defaultMessage": "No past answers." + }, + "course.assessment.submission.answers.AnswerHeader.viewAllAnswers": { + "defaultMessage": "All Answers ({count})" + }, + "course.assessment.submission.answers.AnswerHeader.viewGetHelpHistory": { + "defaultMessage": "Get Help History ({count})" + }, + "course.assessment.submission.answers.AnswerHeader.viewPastAnswers": { + "defaultMessage": "Past Answers ({count})" + }, "course.assessment.submission.answers.ForumPostResponse.ForumCard.forumCardTitleTypeNoneSelected": { "defaultMessage": "Forum" }, @@ -3557,17 +4175,11 @@ "course.assessment.submission.answers.ForumPostResponse.TopicCard.viewTopicInNewTab": { "defaultMessage": "View Topic" }, - "course.assessment.submission.answers.Programming.ProgrammingFile.downloadFile": { - "defaultMessage": "Download File" - }, "course.assessment.submission.answers.Programming.ProgrammingFile.sizeTooBig": { "defaultMessage": "The file is too big and cannot be displayed." }, - "course.assessment.submission.answerTooLarge": { - "defaultMessage": "Answer Too Large" - }, - "course.assessment.submission.answerTooLargeError": { - "defaultMessage": "Your answer must be less than 2 MB." + "course.assessment.submission.attachmentRequired": { + "defaultMessage": "*please upload AT LEAST 1 file for this question" }, "course.assessment.submission.attemptedAt": { "defaultMessage": "Attempted At" @@ -3590,14 +4202,17 @@ "course.assessment.submission.bonusEndAt": { "defaultMessage": "Bonus End At" }, - "course.assessment.submission.codaveriAutogradeFailure": { - "defaultMessage": "There is an error while evaluating your code in Codaveri. Try submitting your code again in a couple of minutes or check the error message in the network response." + "course.assessment.submission.category": { + "defaultMessage": "Category" }, - "course.assessment.submission.liveFeedbackNoneGenerated": { - "defaultMessage": "Question {questionIndex}: No feedback generated." + "course.assessment.submission.checkAnswer": { + "defaultMessage": "Check Answer" + }, + "course.assessment.submission.checkAnswerWithLimit": { + "defaultMessage": "Check Answer ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" }, - "course.assessment.submission.liveFeedbackSuccess": { - "defaultMessage": "Question {questionIndex}: Feedback successfully generated." + "course.assessment.submission.codaveriAutogradeFailure": { + "defaultMessage": "There is an error while evaluating your code in Codaveri. Try submitting your code again in a couple of minutes or check the error message in the network response." }, "course.assessment.submission.comment.CodaveriCommentCard.finalise": { "defaultMessage": "Finalise and Post Feedback" @@ -3623,14 +4238,14 @@ "course.assessment.submission.comment.CommentCard.deleteConfirmation": { "defaultMessage": "Are you sure you want to delete this comment?" }, - "course.assessment.submission.comment.CommentCard.save": { - "defaultMessage": "Save" + "course.assessment.submission.comment.CommentCard.isAiGenerated": { + "defaultMessage": "AI Generated Comment" }, "course.assessment.submission.comment.CommentCard.publish": { "defaultMessage": "Publish" }, - "course.assessment.submission.comment.CommentCard.isAiGenerated": { - "defaultMessage": "AI Generated Comment" + "course.assessment.submission.comment.CommentCard.save": { + "defaultMessage": "Save" }, "course.assessment.submission.comment.CommentField.comment": { "defaultMessage": "Comment" @@ -3647,18 +4262,6 @@ "course.assessment.submission.comments": { "defaultMessage": "Comments" }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete": { - "defaultMessage": "Dismiss" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike": { - "defaultMessage": "Dislike" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike": { - "defaultMessage": "Like" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading": { - "defaultMessage": "Line {linenum}" - }, "course.assessment.submission.continue": { "defaultMessage": "Continue" }, @@ -3704,6 +4307,9 @@ "course.assessment.submission.emptyAssessment": { "defaultMessage": "This assessment currently has no questions." }, + "course.assessment.submission.errorUnknown": { + "defaultMessage": "Error is Unknown" + }, "course.assessment.submission.examDialogMessage": { "defaultMessage": "Please do not sign out or close the browser, otherwise you may have trouble continuing the exam." }, @@ -3713,6 +4319,18 @@ "course.assessment.submission.expAwarded": { "defaultMessage": "EXP Awarded" }, + "course.assessment.submission.explanation": { + "defaultMessage": "Explanation" + }, + "course.assessment.submission.fetchSubmissionsFromKoditsuConfirmation": { + "defaultMessage": "Are you sure you want to fetch all submissions from Koditsu? all the existing answers here will be overwritten by the newer one. NOTE THAT THIS ACTION IS IRREVERSIBLE!" + }, + "course.assessment.submission.fetchSubmissionsFromKoditsuPending": { + "defaultMessage": "Please wait as the submissions are currently being fetched from Koditsu." + }, + "course.assessment.submission.fetchSubmissionsFromKoditsuSuccess": { + "defaultMessage": "All submissions have been fetched successfully from Koditsu" + }, "course.assessment.submission.finalise": { "defaultMessage": "Finalise all answers" }, @@ -3743,6 +4361,9 @@ "course.assessment.submission.grade": { "defaultMessage": "Grade" }, + "course.assessment.submission.gradeDisplay": { + "defaultMessage": "Grade: {grade}" + }, "course.assessment.submission.gradePrefilled": { "defaultMessage": "Pre-filled" }, @@ -3755,9 +4376,6 @@ "course.assessment.submission.gradeSummary": { "defaultMessage": "Grade Summary" }, - "course.assessment.submission.gradeUnsaved": { - "defaultMessage": "Unsaved" - }, "course.assessment.submission.gradeUnsavedHint": { "defaultMessage": "This grade is not yet saved. Click Save Grade at the end of the page to save all grade changes." }, @@ -3773,6 +4391,12 @@ "course.assessment.submission.group": { "defaultMessage": "Group" }, + "course.assessment.submission.history.questionTitle": { + "defaultMessage": "Question Details" + }, + "course.assessment.submission.history.title": { + "defaultMessage": "Submission by {studentName}, Question {number}" + }, "course.assessment.submission.importFilesFailure": { "defaultMessage": "File uploads failed: {errors}" }, @@ -3782,9 +4406,24 @@ "course.assessment.submission.invalidFileUpload": { "defaultMessage": "File uploads failed: Only java files can be uploaded" }, + "course.assessment.submission.isSaved": { + "defaultMessage": "Saved" + }, + "course.assessment.submission.isSaving": { + "defaultMessage": "Saving" + }, + "course.assessment.submission.isUnsaved": { + "defaultMessage": "Unsaved" + }, "course.assessment.submission.lateSubmission": { "defaultMessage": "This submission is LATE! You may want to penalize the student for late submission." }, + "course.assessment.submission.liveFeedbackHistory.codeHistory": { + "defaultMessage": "Code History" + }, + "course.assessment.submission.liveFeedbackNoneGenerated": { + "defaultMessage": "No feedback generated." + }, "course.assessment.submission.loadingComment": { "defaultMessage": "Loading comment field..." }, @@ -3821,6 +4460,9 @@ "course.assessment.submission.mark": { "defaultMessage": "Submit for Publishing" }, + "course.assessment.submission.max": { + "defaultMessage": "Max" + }, "course.assessment.submission.maximumGroupGrade": { "defaultMessage": "Maximum Grade for this Group" }, @@ -3833,6 +4475,9 @@ "course.assessment.submission.ok": { "defaultMessage": "OK" }, + "course.assessment.submission.onlyOneAttachmentAllowed": { + "defaultMessage": "*ONLY 1 file is allowed for this question" + }, "course.assessment.submission.pastAnswers": { "defaultMessage": "Past Answers" }, @@ -3875,14 +4520,20 @@ "course.assessment.submission.question": { "defaultMessage": "Question" }, - "course.assessment.submission.questionNumber": { - "defaultMessage": "Q{number}" + "course.assessment.submission.questionAnswer": { + "defaultMessage": "Answer" }, "course.assessment.submission.questionDescription": { "defaultMessage": "Description" }, - "course.assessment.submission.questionAnswer": { - "defaultMessage": "Answer" + "course.assessment.submission.questionHeading": { + "defaultMessage": "Question {number}" + }, + "course.assessment.submission.questionHeadingWithTitle": { + "defaultMessage": "Question {number}: {title}" + }, + "course.assessment.submission.questionNumber": { + "defaultMessage": "Q{number}" }, "course.assessment.submission.readOnlyEditor.expandComments": { "defaultMessage": "Expand all comments" @@ -3893,6 +4544,12 @@ "course.assessment.submission.reevaluate": { "defaultMessage": "Re-evaluate Answer" }, + "course.assessment.submission.remainingBufferTime": { + "defaultMessage": "Finalising in: {timeLimit}" + }, + "course.assessment.submission.remainingTime": { + "defaultMessage": "Time Remaining: {timeLimit}" + }, "course.assessment.submission.rendererNotImplemented": { "defaultMessage": "The display for this question type has not been implemented yet." }, @@ -3905,14 +4562,8 @@ "course.assessment.submission.resetConfirmation": { "defaultMessage": "Are you sure you want to reset your answer? This action is irreversible and you will lose all your current work for this question." }, - "course.assessment.submission.checkAnswer": { - "defaultMessage": "Check Answer" - }, - "course.assessment.submission.checkAnswerWithLimit": { - "defaultMessage": "Check Answer ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" - }, - "course.assessment.submission.submitWithLimit": { - "defaultMessage": "Submit ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" + "course.assessment.submission.rubricScores": { + "defaultMessage": "Rubric Grades" }, "course.assessment.submission.saveDraft": { "defaultMessage": "Save Draft" @@ -3920,6 +4571,15 @@ "course.assessment.submission.saveGrade": { "defaultMessage": "Save Grade" }, + "course.assessment.submission.saved": { + "defaultMessage": "Saved" + }, + "course.assessment.submission.saving": { + "defaultMessage": "Saving" + }, + "course.assessment.submission.savingFailed": { + "defaultMessage": "Saving Failed" + }, "course.assessment.submission.sendReminderEmailConfirmation": { "defaultMessage": "Send reminder emails to {unattempted} unattempted and {attempting} attempting user(s) ({selectedUsers}) who have not completed the assessment?" }, @@ -3956,6 +4616,9 @@ "course.assessment.submission.submissionBy": { "defaultMessage": "Submission by {name}" }, + "course.assessment.submission.submissionError": { + "defaultMessage": "There is a problem in submitting question for {questions}" + }, "course.assessment.submission.submissionsHeader": { "defaultMessage": "Submissions: {assessment}" }, @@ -3968,20 +4631,56 @@ "course.assessment.submission.submitShortcut": { "defaultMessage": "(Ctrl+Enter) or (⌘+Enter)" }, + "course.assessment.submission.submitWithLimit": { + "defaultMessage": "Submit ({attemptsLeft, plural, one {# attempt} other {# attempts}} left)" + }, "course.assessment.submission.submitted": { "defaultMessage": "Submitted" }, "course.assessment.submission.submittedAt": { "defaultMessage": "Submitted At" }, - "course.assessment.submission.unknown": { - "defaultMessage": "Unknown status, please contact administrator" + "course.assessment.submission.suggestions.howDoIFixThis": { + "defaultMessage": "How do I fix this?" }, - "course.assessment.submission.totalGrade": { - "defaultMessage": "Total Grade" + "course.assessment.submission.suggestions.iAmStuck": { + "defaultMessage": "I am stuck" }, - "course.assessment.submission.type": { - "defaultMessage": "Type" + "course.assessment.submission.suggestions.looksWrong": { + "defaultMessage": "This looks wrong" + }, + "course.assessment.submission.suggestions.optimizeThisCode": { + "defaultMessage": "Review my code" + }, + "course.assessment.submission.suggestions.questionUnclear": { + "defaultMessage": "Explain the question" + }, + "course.assessment.submission.suggestions.whereAmIWrong": { + "defaultMessage": "Where am I wrong?" + }, + "course.assessment.submission.timeIsUp": { + "defaultMessage": "Time is Up!" + }, + "course.assessment.submission.timedAssessmentDialogMessage": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {Once the time is up, the assessment will be automatically finalised.} other {Finalising the submission now!}}" + }, + "course.assessment.submission.timedAssessmentDialogTitle": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {{remainingTime} {isNewSubmission, select, true {} other {remaining}} to complete this assessment.} other {The assessment has ended!}}" + }, + "course.assessment.submission.timedExamDialogMessage": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {Please do not sign out or close the browser while attempting this exam. Once the time is up, the assessment will be automatically finalised.} other {Finalising the submission now!}}" + }, + "course.assessment.submission.timedExamDialogTitle": { + "defaultMessage": "{stillSomeTimeRemaining, select, true {{remainingTime} {isNewSubmission, select, true {} other {remaining}} to complete this exam.} other {The exam has ended!}}" + }, + "course.assessment.submission.totalGrade": { + "defaultMessage": "Total Grade" + }, + "course.assessment.submission.type": { + "defaultMessage": "Type" + }, + "course.assessment.submission.unknown": { + "defaultMessage": "Unknown status, please contact administrator" }, "course.assessment.submission.unmark": { "defaultMessage": "Revert to Submitted" @@ -3996,7 +4695,7 @@ "defaultMessage": "Unsubmit Submission" }, "course.assessment.submission.unsubmitAllConfirmation": { - "defaultMessage": "Are you sure you want to UNSUBMIT the submissions for all {users}? All submissions will be unsubmitted and this will reset the submission time and permit the users to change their answers. NOTE THAT THIS ACTION IS IRREVERSIBLE" + "defaultMessage": "Are you sure you want to UNSUBMIT the submissions for all {users}? All submissions will be unsubmitted and this will reset the submission time and permit the users to change their submissions. NOTE THAT THIS ACTION IS IRREVERSIBLE" }, "course.assessment.submission.unsubmitAllSubmissionsJobPending": { "defaultMessage": "Please wait as the submissions are currently being unsubmitted." @@ -4013,6 +4712,9 @@ "course.assessment.submission.updateFailure": { "defaultMessage": "Submission update failed: {errors}" }, + "course.assessment.submission.updateIndividualSuccess": { + "defaultMessage": "Submission for {errors} updated successfully" + }, "course.assessment.submission.updateSuccess": { "defaultMessage": "Submission updated successfully." }, @@ -4049,9 +4751,6 @@ "course.assessment.submissions.SubmissionsIndex.header": { "defaultMessage": "Submissions" }, - "course.assessment.submission.SubmissionsIndex.publishAutoFeedback": { - "defaultMessage": "Publish Automated Programming Feedback ({count})" - }, "course.assessment.submissions.SubmissionsTable.gradeTooltip": { "defaultMessage": "These grades can't be seen by the student until they are published" }, @@ -4074,7 +4773,7 @@ "defaultMessage": "Submitted At" }, "course.assessment.submissions.SubmissionsTable.tableHeaderTitle": { - "defaultMessage": "Assessment" + "defaultMessage": "Title" }, "course.assessment.submissions.SubmissionsTable.tableHeaderTotalGrade": { "defaultMessage": "Grade" @@ -4133,6 +4832,18 @@ "course.assessments.index.hasTodo": { "defaultMessage": "Has TODO" }, + "course.assessments.index.inviteToKoditsu": { + "defaultMessage": "Invite users to Koditsu Exam" + }, + "course.assessments.index.invitingUserToKoditsu": { + "defaultMessage": "Inviting users to Koditsu Exam" + }, + "course.assessments.index.invitingUserToKoditsuFailure": { + "defaultMessage": "There is a problem in inviting users to Koditsu. Please try again later" + }, + "course.assessments.index.invitingUserToKoditsuSuccess": { + "defaultMessage": "Successful in inviting users to Koditsu Exam" + }, "course.assessments.index.neededFor": { "defaultMessage": "Needed for" }, @@ -4163,6 +4874,9 @@ "course.assessments.index.submittedCount": { "defaultMessage": "Submissions" }, + "course.assessments.index.timeLimitIcon": { + "defaultMessage": "Time Limit: {timeLimit, plural, one {# minute} other {# minutes}}" + }, "course.assessments.index.title": { "defaultMessage": "Title" }, @@ -4181,9 +4895,6 @@ "course.asssessment.submission.submitNoQuestionExplain": { "defaultMessage": "Mark as completed?" }, - "course.admin.NotificationSettings.component": { - "defaultMessage": "Component" - }, "course.componentTitles.course_achievements_component": { "defaultMessage": "Achievements" }, @@ -4268,15 +4979,6 @@ "course.courses.CourseAnnouncements.announcementHeader": { "defaultMessage": "Latest announcements" }, - "course.courses.CourseSuspendedAlert.header": { - "defaultMessage": "This course is suspended. Instructors can still access it, but students cannot." - }, - "course.courses.CourseSuspendedAlert.canSuspendMessage": { - "defaultMessage": "You can unsuspend it from the {link} page." - }, - "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { - "defaultMessage": "If you believe this is a mistake, contact a course manager or owner to have them unsuspend the course." - }, "course.courses.CourseDisplay.noCourse": { "defaultMessage": "There is no course yet..." }, @@ -4322,6 +5024,15 @@ "course.courses.CourseShow.instructorsHeader": { "defaultMessage": "Instructors" }, + "course.courses.CourseSuspendedAlert.canSuspendMessage": { + "defaultMessage": "You can unsuspend it from the {link} page." + }, + "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { + "defaultMessage": "If you believe this is a mistake, contact a course manager or owner to have them unsuspend the course." + }, + "course.courses.CourseSuspendedAlert.header": { + "defaultMessage": "This course is suspended. Instructors can still access it, but students cannot." + }, "course.courses.CourseUserItem.differentCourseNameHint": { "defaultMessage": "You're seeing a name different from your account name because this course's manager invited you with this name." }, @@ -4437,7 +5148,7 @@ "defaultMessage": "Starts at" }, "course.courses.PendingTodosTable.tableHeaderTitle": { - "defaultMessage": "Title" + "defaultMessage": "Assessment" }, "course.courses.PendingTodosTable.tableSeeMore": { "defaultMessage": "See {n} more" @@ -4445,6 +5156,9 @@ "course.courses.Sidebar.administration": { "defaultMessage": "Administration" }, + "course.courses.Sidebar.joinCoursemologyMessage": { + "defaultMessage": "Create a Coursemology account or sign up to join this course." + }, "course.courses.SidebarItem.admin.duplication": { "defaultMessage": "Duplicate Data" }, @@ -4490,15 +5204,15 @@ "course.courses.SidebarItem.home": { "defaultMessage": "Home" }, + "course.courses.SidebarItem.scholaistic.assessments": { + "defaultMessage": "Role-Playing Assessments" + }, "course.courses.SidebarItem.stories.learn": { "defaultMessage": "Learn" }, "course.courses.SidebarItem.stories.missionControl": { "defaultMessage": "Mission Control" }, - "course.courses.SidebarItem.scholaistic.assessments": { - "defaultMessage": "Role-Playing Assessments" - }, "course.courses.TodoIgnoreButton.ignore.ignoreButtonText": { "defaultMessage": "Ignore" }, @@ -4541,6 +5255,12 @@ "course.discussion.topics.CommentCard.deleteSuccess": { "defaultMessage": "Successfully deleted comment." }, + "course.discussion.topics.CommentCard.isAiGenerated": { + "defaultMessage": "AI Generated Comment" + }, + "course.discussion.topics.CommentCard.publish": { + "defaultMessage": "Publish" + }, "course.discussion.topics.CommentCard.publishFailure": { "defaultMessage": "Failed to publish feedback." }, @@ -4562,12 +5282,6 @@ "course.discussion.topics.CommentCard.updateSuccess": { "defaultMessage": "Successfully updated comment." }, - "course.discussion.topics.CommentCard.publish": { - "defaultMessage": "Publish" - }, - "course.discussion.topics.CommentCard.isAiGenerated": { - "defaultMessage": "AI Generated Comment" - }, "course.discussion.topics.CommentField.comment": { "defaultMessage": "Comment" }, @@ -4635,7 +5349,7 @@ "defaultMessage": "Select current instance" }, "course.duplication.Duplication.DestinationCourseSelector.InstanceDropdown.destinationInstance": { - "defaultMessage": "Destination Instance" + "defaultMessage": "Destination instance" }, "course.duplication.Duplication.DestinationCourseSelector.NewCourseForm.newStartAt": { "defaultMessage": "New Start Date *" @@ -4739,15 +5453,15 @@ "course.duplication.Duplication.duplicateData": { "defaultMessage": "Duplicate Data" }, - "course.duplication.Duplication.fromCourse": { - "defaultMessage": "Duplicate Data from {courseTitle}" - }, "course.duplication.Duplication.duplicationDisabled": { "defaultMessage": "Duplication is disabled for this course." }, "course.duplication.Duplication.existingCourse": { "defaultMessage": "Existing Course" }, + "course.duplication.Duplication.fromCourse": { + "defaultMessage": "Duplicate data from {courseTitle}" + }, "course.duplication.Duplication.items": { "defaultMessage": "Selected Items" }, @@ -4841,593 +5555,971 @@ "course.enrolRequests.UserRequests.rejected": { "defaultMessage": "Rejected Enrolment Requests" }, + "course.experiencePoints.disbursement.DisbursementForm.createDisbursementFailure": { + "defaultMessage": "Failed to award experience points." + }, + "course.experiencePoints.disbursement.DisbursementForm.createDisbursementSuccess": { + "defaultMessage": "Experience points disbursed to {recipientCount} recipients." + }, + "course.experiencePoints.disbursement.DisbursementForm.fetchDisbursementFailure": { + "defaultMessage": "Failed to retrieve data." + }, + "course.experiencePoints.disbursement.DisbursementForm.fetchFilterFailure": { + "defaultMessage": "Failed to retrieve filtered forum users." + }, + "course.experiencePoints.disbursement.DisbursementForm.fetchFilterNone": { + "defaultMessage": "No post made between these 2 dates." + }, + "course.experiencePoints.disbursement.DisbursementForm.filter": { + "defaultMessage": "Filter by group" + }, + "course.experiencePoints.disbursement.DisbursementForm.noDisbursement": { + "defaultMessage": "No points are disbursed to users." + }, + "course.experiencePoints.disbursement.DisbursementForm.notNumber": { + "defaultMessage": "Not a Number." + }, + "course.experiencePoints.disbursement.DisbursementForm.reason": { + "defaultMessage": "Reason For Disbursement" + }, + "course.experiencePoints.disbursement.DisbursementForm.submit": { + "defaultMessage": "Disburse Points" + }, + "course.experiencePoints.disbursement.DisbursementIndex.disbursements": { + "defaultMessage": "Experience Points" + }, + "course.experiencePoints.disbursement.DisbursementIndex.experienceTab": { + "defaultMessage": "History" + }, + "course.experiencePoints.disbursement.DisbursementIndex.fetchDisbursementFailure": { + "defaultMessage": "Failed to retrieve data." + }, + "course.experiencePoints.disbursement.DisbursementIndex.forumTab": { + "defaultMessage": "Forum Participation" + }, + "course.experiencePoints.disbursement.DisbursementIndex.generalTab": { + "defaultMessage": "General Disbursement" + }, + "course.experiencePoints.disbursement.DisbursementTable.copy": { + "defaultMessage": "Copy value for all students" + }, + "course.experiencePoints.disbursement.DisbursementTable.name": { + "defaultMessage": "Name" + }, + "course.experiencePoints.disbursement.DisbursementTable.pointsAwarded": { + "defaultMessage": "EXP Awarded" + }, + "course.experiencePoints.disbursement.DisbursementTable.remove": { + "defaultMessage": "Remove value for all students" + }, + "course.experiencePoints.disbursement.FilterForm.endTime": { + "defaultMessage": "End Date *" + }, + "course.experiencePoints.disbursement.FilterForm.startTime": { + "defaultMessage": "Start Date *" + }, + "course.experiencePoints.disbursement.FilterForm.submit": { + "defaultMessage": "Search" + }, + "course.experiencePoints.disbursement.FilterForm.weeklyCap": { + "defaultMessage": "Weekly Cap" + }, + "course.experiencePoints.disbursement.ForumDisbursement.fetchDisbursementFailure": { + "defaultMessage": "Failed to retrieve data." + }, + "course.experiencePoints.disbursement.ForumDisbursement.fetchForumPostsFailure": { + "defaultMessage": "Failed to fetch forum posts." + }, + "course.experiencePoints.disbursement.ForumDisbursement.postListDialogHeader": { + "defaultMessage": "Posts created between {startDate} and {endDate} by" + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.createDisbursementFailure": { + "defaultMessage": "Failed to award experience points." + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.createDisbursementSuccess": { + "defaultMessage": "Experience points disbursed to {recipientCount} recipients." + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.fetchForumPostsFailure": { + "defaultMessage": "Failed to fetch forum posts." + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.reason": { + "defaultMessage": "Reason For Disbursement" + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.reasonFill": { + "defaultMessage": "Forum Participation" + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.submit": { + "defaultMessage": "Disburse Points" + }, + "course.experiencePoints.disbursement.ForumDisbursementForm.viewPosts": { + "defaultMessage": "View Forum Posts" + }, + "course.experiencePoints.disbursement.ForumDisbursementTable.exp": { + "defaultMessage": "Experience Points" + }, + "course.experiencePoints.disbursement.ForumDisbursementTable.level": { + "defaultMessage": "Level" + }, + "course.experiencePoints.disbursement.ForumDisbursementTable.name": { + "defaultMessage": "Name" + }, + "course.experiencePoints.disbursement.ForumDisbursementTable.pointsAwarded": { + "defaultMessage": "EXP Awarded" + }, + "course.experiencePoints.disbursement.ForumDisbursementTable.postCount": { + "defaultMessage": "Post Count" + }, + "course.experiencePoints.disbursement.ForumDisbursementTable.voteTally": { + "defaultMessage": "Vote Tally" + }, + "course.experiencePoints.disbursement.ForumPostTable.datePosted": { + "defaultMessage": "Date Posted" + }, + "course.experiencePoints.disbursement.ForumPostTable.topicTitle": { + "defaultMessage": "Topic Title" + }, + "course.experiencePoints.disbursement.ForumPostTable.voteTally": { + "defaultMessage": "Vote Tally" + }, + "course.experiencePoints.disbursement.GeneralDisbursement.fetchDisbursementFailure": { + "defaultMessage": "Failed to retrieve data." + }, "course.experiencePoints.downloadCsvButton": { "defaultMessage": "Download CSV" }, "course.experiencePoints.downloadFailure": { "defaultMessage": "An error occurred while doing your request for download." }, - "course.experiencePoints.downloadPending": { - "defaultMessage": "Please wait as your request to download is being processed." + "course.experiencePoints.downloadPending": { + "defaultMessage": "Please wait as your request to download is being processed." + }, + "course.experiencePoints.downloadRequestSuccess": { + "defaultMessage": "Your request to download is successful" + }, + "course.experiencePoints.fetchRecordsFailure": { + "defaultMessage": "Failed to fetch records" + }, + "course.experiencePoints.filterByNameButton": { + "defaultMessage": "Filter by Name" + }, + "course.forum.FormShow.fetchTopicsFailure": { + "defaultMessage": "Failed to retrieve forum topic data." + }, + "course.forum.FormShow.header": { + "defaultMessage": "Forum Topics" + }, + "course.forum.FormShow.markAllAsReadSuccess": { + "defaultMessage": "All topics in this forum have been marked as read." + }, + "course.forum.FormShow.newTopic": { + "defaultMessage": "New Topic" + }, + "course.forum.ForumEdit.editForum": { + "defaultMessage": "Edit Forum" + }, + "course.forum.ForumEdit.updateFailure": { + "defaultMessage": "Failed to update the forum." + }, + "course.forum.ForumEdit.updateSuccess": { + "defaultMessage": "Forum {title} has been updated." + }, + "course.forum.ForumForm.description": { + "defaultMessage": "Description" + }, + "course.forum.ForumForm.forumTopicsAutoSubscribe": { + "defaultMessage": "Enable auto-subscription to a forum topic when a user creates the topic, new posts or replies." + }, + "course.forum.ForumForm.name": { + "defaultMessage": "Name" + }, + "course.forum.ForumManagementButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete this forum \"{title}\"?" + }, + "course.forum.ForumManagementButtons.deletionFailure": { + "defaultMessage": "Failed to delete forum - {error}" + }, + "course.forum.ForumManagementButtons.deletionSuccess": { + "defaultMessage": "Forum {title} was deleted." + }, + "course.forum.ForumNew.creationFailure": { + "defaultMessage": "Failed to create forum." + }, + "course.forum.ForumNew.creationSuccess": { + "defaultMessage": "Forum {title} has been created." + }, + "course.forum.ForumNew.newForum": { + "defaultMessage": "New Forum" + }, + "course.forum.ForumTable.autoSubscribe": { + "defaultMessage": "Users will be automatically subscribed to a topic in this forum when they create a post in the topic." + }, + "course.forum.ForumTable.forum": { + "defaultMessage": "Forum" + }, + "course.forum.ForumTable.hasUnresolved": { + "defaultMessage": "Has unresolved question(s)" + }, + "course.forum.ForumTable.isSubscribed": { + "defaultMessage": "Subscribed?" + }, + "course.forum.ForumTable.noForum": { + "defaultMessage": "No Forum" + }, + "course.forum.ForumTable.posts": { + "defaultMessage": "Posts" + }, + "course.forum.ForumTable.topics": { + "defaultMessage": "Topics" + }, + "course.forum.ForumTable.views": { + "defaultMessage": "Views" + }, + "course.forum.ForumTable.votes": { + "defaultMessage": "Votes" + }, + "course.forum.ForumTopicEdit.editForum": { + "defaultMessage": "Edit Topic" + }, + "course.forum.ForumTopicEdit.updateFailure": { + "defaultMessage": "Failed to update the topic." + }, + "course.forum.ForumTopicEdit.updateSuccess": { + "defaultMessage": "Topic {title} has been updated." + }, + "course.forum.ForumTopicForm.postAnonymously": { + "defaultMessage": "Anonymous post" + }, + "course.forum.ForumTopicForm.text": { + "defaultMessage": "Text" + }, + "course.forum.ForumTopicForm.title": { + "defaultMessage": "Title" + }, + "course.forum.ForumTopicForm.topicType": { + "defaultMessage": "Topic Type" + }, + "course.forum.ForumTopicForm.topicType.announcement": { + "defaultMessage": "Announcement" + }, + "course.forum.ForumTopicForm.topicType.normal": { + "defaultMessage": "Normal" + }, + "course.forum.ForumTopicForm.topicType.question": { + "defaultMessage": "Question" + }, + "course.forum.ForumTopicForm.topicType.sticky": { + "defaultMessage": "Sticky" + }, + "course.forum.ForumTopicManagementButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete this topic \"{title}\"?" + }, + "course.forum.ForumTopicManagementButtons.deletionFailure": { + "defaultMessage": "Failed to delete topic - {error}" + }, + "course.forum.ForumTopicManagementButtons.deletionSuccess": { + "defaultMessage": "Topic {title} was deleted." + }, + "course.forum.ForumTopicNew.creationFailure": { + "defaultMessage": "Failed to create topic." + }, + "course.forum.ForumTopicNew.creationSuccess": { + "defaultMessage": "Topic {title} has been created." + }, + "course.forum.ForumTopicNew.newTopic": { + "defaultMessage": "New Topic" + }, + "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptAction": { + "defaultMessage": "Discard" + }, + "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptMessage": { + "defaultMessage": "You have edited this post and there are unsaved changes. Do you wish to proceed?" + }, + "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptTitle": { + "defaultMessage": "Discard unsaved changes?" + }, + "course.forum.ForumTopicPostEditActionButtons.emptyPost": { + "defaultMessage": "Post cannot be empty!" + }, + "course.forum.ForumTopicPostEditActionButtons.updateFailure": { + "defaultMessage": "Failed to update the post - {error}" + }, + "course.forum.ForumTopicPostEditActionButtons.updateSuccess": { + "defaultMessage": "The post has been updated." + }, + "course.forum.ForumTopicPostForm.postAnonymously": { + "defaultMessage": "Anonymous post" + }, + "course.forum.ForumTopicPostManagementButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete this topic post?" + }, + "course.forum.ForumTopicPostManagementButtons.deletionFailure": { + "defaultMessage": "Failed to delete topic - {error}" + }, + "course.forum.ForumTopicPostManagementButtons.deletionSuccess": { + "defaultMessage": "The post has been deleted." + }, + "course.forum.ForumTopicPostNew.creationFailure": { + "defaultMessage": "Failed to create the post - {error}" + }, + "course.forum.ForumTopicPostNew.creationSuccess": { + "defaultMessage": "The post has been created." + }, + "course.forum.ForumTopicPostNew.newPost": { + "defaultMessage": "Create a New Post" + }, + "course.forum.ForumTopicShow.fetchPostsFailure": { + "defaultMessage": "Failed to retrieve forum topic data." + }, + "course.forum.ForumTopicShow.header": { + "defaultMessage": "Forum Topic Posts" + }, + "course.forum.ForumTopicShow.lockedNote": { + "defaultMessage": "You are unable to add new post as this topic has been locked by the teaching staff." + }, + "course.forum.ForumTopicShow.noPosts": { + "defaultMessage": "No Post" + }, + "course.forum.ForumTopicShow.topicResolved": { + "defaultMessage": "This question topic has been resolved." + }, + "course.forum.ForumTopicShow.topicUnresolved": { + "defaultMessage": "This question topic is unresolved." + }, + "course.forum.ForumTopicShow.topicUnresolvedNote": { + "defaultMessage": "Mark helpful post(s) as answer(s) to resolve this question." + }, + "course.forum.ForumTopicTable.hidden": { + "defaultMessage": "This topic is hidden for students." + }, + "course.forum.ForumTopicTable.isSubscribed": { + "defaultMessage": "Subscribed?" + }, + "course.forum.ForumTopicTable.lastPostedBy": { + "defaultMessage": "Last Posted By" + }, + "course.forum.ForumTopicTable.locked": { + "defaultMessage": "This topic is closed; it no longer accepts new replies." + }, + "course.forum.ForumTopicTable.noTopic": { + "defaultMessage": "No Topic" + }, + "course.forum.ForumTopicTable.posts": { + "defaultMessage": "Posts" + }, + "course.forum.ForumTopicTable.resolved": { + "defaultMessage": "Question (Resolved)" + }, + "course.forum.ForumTopicTable.startedBy": { + "defaultMessage": "Started By" + }, + "course.forum.ForumTopicTable.topics": { + "defaultMessage": "Topics" + }, + "course.forum.ForumTopicTable.unresolved": { + "defaultMessage": "Question (Unresolved)" + }, + "course.forum.ForumTopicTable.views": { + "defaultMessage": "Views" + }, + "course.forum.ForumTopicTable.votes": { + "defaultMessage": "Votes" + }, + "course.forum.ForumsIndex.fetchForumsFailure": { + "defaultMessage": "Failed to retrieve forum data." + }, + "course.forum.ForumsIndex.header": { + "defaultMessage": "Forums" + }, + "course.forum.ForumsIndex.markAllAsReadFailed": { + "defaultMessage": "Failed to mark all topics as read. Please try again later." + }, + "course.forum.ForumsIndex.markAllAsReadSuccess": { + "defaultMessage": "All topics have been marked as read." + }, + "course.forum.ForumsIndex.newForum": { + "defaultMessage": "New Forum" }, - "course.experiencePoints.downloadRequestSuccess": { - "defaultMessage": "Your request to download is successful" + "course.forum.GenerateReplyButton.generateReply": { + "defaultMessage": "Generate reply" }, - "course.experiencePoints.filterByNameButton": { - "defaultMessage": "Filter by Name" + "course.forum.GenerateReplyButton.generateReplySuccess": { + "defaultMessage": "A reply has been successfully generated." }, - "course.experiencePoints.disbursement.DisbursementForm.createDisbursementFailure": { - "defaultMessage": "Failed to award experience points." + "course.forum.GenerateReplyButton.generatingReply": { + "defaultMessage": "Generating reply" }, - "course.experiencePoints.disbursement.DisbursementForm.createDisbursementSuccess": { - "defaultMessage": "Experience points disbursed to {recipientCount} recipients." + "course.forum.HideButton.hide": { + "defaultMessage": "Hide" }, - "course.experiencePoints.disbursement.DisbursementForm.fetchDisbursementFailure": { - "defaultMessage": "Failed to retrieve data." + "course.forum.HideButton.hideFailure": { + "defaultMessage": "Failed to hide the topic \"{title}\" - {error}" }, - "course.experiencePoints.disbursement.DisbursementForm.fetchFilterFailure": { - "defaultMessage": "Failed to retrieve filtered forum users." + "course.forum.HideButton.hideSuccess": { + "defaultMessage": "The topic \"{title}\" has successfully been hidden." }, - "course.experiencePoints.disbursement.DisbursementForm.fetchFilterNone": { - "defaultMessage": "No post made between these 2 dates." + "course.forum.HideButton.hideTooltip": { + "defaultMessage": "Hide topic from students" }, - "course.experiencePoints.disbursement.DisbursementForm.filter": { - "defaultMessage": "Filter by group" + "course.forum.HideButton.unhide": { + "defaultMessage": "Unhide" }, - "course.experiencePoints.disbursement.DisbursementForm.noDisbursement": { - "defaultMessage": "No points are disbursed to users." + "course.forum.HideButton.unhideFailure": { + "defaultMessage": "Failed to unhide the topic \"{title}\" - {error}" }, - "course.experiencePoints.disbursement.DisbursementForm.notNumber": { - "defaultMessage": "Not a Number." + "course.forum.HideButton.unhideSuccess": { + "defaultMessage": "The topic \"{title}\" has successfully been unhidden." }, - "course.experiencePoints.disbursement.DisbursementForm.reason": { - "defaultMessage": "Reason For Disbursement" + "course.forum.HideButton.unhideTooltip": { + "defaultMessage": "Show topic to students" }, - "course.experiencePoints.disbursement.DisbursementForm.submit": { - "defaultMessage": "Disburse Points" + "course.forum.LockButton.lockTooltip": { + "defaultMessage": "Lock to stop students from posting in this topic" }, - "course.experiencePoints.disbursement.DisbursementIndex.disbursements": { - "defaultMessage": "Disbursed Experience Points" + "course.forum.LockButton.locked": { + "defaultMessage": "Lock" }, - "course.experiencePoints.disbursement.DisbursementIndex.experienceTab": { - "defaultMessage": "History" + "course.forum.LockButton.lockedFailure": { + "defaultMessage": "Failed to locked the topic \"{title}\" - {error}" }, - "course.experiencePoints.disbursement.DisbursementIndex.fetchDisbursementFailure": { - "defaultMessage": "Failed to retrieve data." + "course.forum.LockButton.lockedSuccess": { + "defaultMessage": "The topic \"{title}\" has successfully been locked." }, - "course.experiencePoints.disbursement.DisbursementIndex.forumTab": { - "defaultMessage": "Forum Participation" + "course.forum.LockButton.unlockTooltip": { + "defaultMessage": "Unlock to allow students to post within this topic" }, - "course.experiencePoints.disbursement.DisbursementIndex.generalTab": { - "defaultMessage": "General Disbursement" + "course.forum.LockButton.unlocked": { + "defaultMessage": "Unlock" }, - "course.experiencePoints.disbursement.DisbursementTable.copy": { - "defaultMessage": "Copy value for all students" + "course.forum.LockButton.unlockedFailure": { + "defaultMessage": "Failed to unlocked the topic \"{title}\" - {error}" }, - "course.experiencePoints.disbursement.DisbursementTable.name": { - "defaultMessage": "Name" + "course.forum.LockButton.unlockedSuccess": { + "defaultMessage": "The topic \"{title}\" has successfully been unlocked." }, - "course.experiencePoints.disbursement.DisbursementTable.pointsAwarded": { - "defaultMessage": "EXP Awarded" + "course.forum.MarkAllAsReadButton.AllReadTooltip": { + "defaultMessage": "Hooray! All topics have been read!" }, - "course.experiencePoints.disbursement.DisbursementTable.remove": { - "defaultMessage": "Remove value for all students" + "course.forum.MarkAllAsReadButton.markAllAsRead": { + "defaultMessage": "Mark all as read" }, - "course.experiencePoints.disbursement.FilterForm.endTime": { - "defaultMessage": "End Date *" + "course.forum.MarkAllAsReadButton.markAllAsReadTooltip": { + "defaultMessage": "Mark all forum posts on the current page as read" }, - "course.experiencePoints.disbursement.FilterForm.startTime": { - "defaultMessage": "Start Date *" + "course.forum.MarkAnswerButton.markAsAnswer": { + "defaultMessage": "Mark as answer" }, - "course.experiencePoints.disbursement.FilterForm.submit": { - "defaultMessage": "Search" + "course.forum.MarkAnswerButton.markAsAnswerAndPublish": { + "defaultMessage": "Mark as answer and publish" }, - "course.experiencePoints.disbursement.FilterForm.weeklyCap": { - "defaultMessage": "Weekly Cap" + "course.forum.MarkAnswerButton.markAsAnswerAndPublishTooltip": { + "defaultMessage": "Mark as answer and publish for students to view" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.createDisbursementFailure": { - "defaultMessage": "Failed to award experience points." + "course.forum.MarkAnswerButton.markedAsAnswer": { + "defaultMessage": "Marked as answer" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.createDisbursementSuccess": { - "defaultMessage": "Experience points disbursed to {recipientCount} recipients." + "course.forum.MarkAnswerButton.unmarkAsAnswer": { + "defaultMessage": "Unmark as answer" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.fetchForumPostsFailure": { - "defaultMessage": "Failed to fetch forum posts." + "course.forum.MarkAnswerButton.updateFailure": { + "defaultMessage": "Failed to update the post - {error}" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.postListDialogHeader": { - "defaultMessage": "Posts created between {startDate} and {endDate} by" + "course.forum.NextUnreadButton.AllReadTooltip": { + "defaultMessage": "Hooray! All topics have been read!" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.reason": { - "defaultMessage": "Reason For Disbursement" + "course.forum.NextUnreadButton.nextUnread": { + "defaultMessage": "Next Unread" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.reasonFill": { - "defaultMessage": "Forum Participation" + "course.forum.NextUnreadButton.nextUnreadTooltip": { + "defaultMessage": "Jump to next unread topic" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.submit": { - "defaultMessage": "Disburse Points" + "course.forum.PostCreatorObject.anonymousUser": { + "defaultMessage": "Anonymous User" }, - "course.experiencePoints.disbursement.ForumDisbursementForm.viewPosts": { - "defaultMessage": "View Forum Posts" + "course.forum.PostCreatorObject.maskUser": { + "defaultMessage": "Mask User" }, - "course.experiencePoints.disbursement.ForumDisbursementTable.exp": { - "defaultMessage": "Experience Points" + "course.forum.PostCreatorObject.postAnonymously": { + "defaultMessage": "Anonymous post" }, - "course.experiencePoints.disbursement.ForumDisbursementTable.level": { - "defaultMessage": "Level" + "course.forum.PostCreatorObject.unmaskUser": { + "defaultMessage": "Unmask User" }, - "course.experiencePoints.disbursement.ForumDisbursementTable.name": { - "defaultMessage": "Name" + "course.forum.ReplyCard.emptyPost": { + "defaultMessage": "Post cannot be empty!" }, - "course.experiencePoints.disbursement.ForumDisbursementTable.pointsAwarded": { - "defaultMessage": "EXP Awarded" + "course.forum.ReplyCard.postAnonymously": { + "defaultMessage": "Anonymous post" }, - "course.experiencePoints.disbursement.ForumDisbursementTable.postCount": { - "defaultMessage": "Post Count" + "course.forum.ReplyCard.replyFailure": { + "defaultMessage": "Failed to submit the post - {error}" }, - "course.experiencePoints.disbursement.ForumDisbursementTable.voteTally": { - "defaultMessage": "Vote Tally" + "course.forum.ReplyCard.replySuccess": { + "defaultMessage": "The reply post has been created." }, - "course.experiencePoints.disbursement.ForumPostTable.datePosted": { - "defaultMessage": "Date Posted" + "course.forum.ReplyCard.replyTo": { + "defaultMessage": "Reply to {user}" }, - "course.experiencePoints.disbursement.ForumPostTable.topicTitle": { - "defaultMessage": "Topic Title" + "course.forum.SubscribeButton.commonTranslations.manageMySubscription": { + "defaultMessage": "Manage My Subscriptions" }, - "course.experiencePoints.disbursement.ForumPostTable.voteTally": { - "defaultMessage": "Vote Tally" + "course.forum.SubscribeButton.commonTranslations.subscribe": { + "defaultMessage": "Subscribe" }, - "course.forum.FormShow.fetchTopicsFailure": { - "defaultMessage": "Failed to retrieve forum topic data." + "course.forum.SubscribeButton.commonTranslations.unsubscribe": { + "defaultMessage": "Unsubscribe" }, - "course.forum.FormShow.header": { - "defaultMessage": "Forum Topics" + "course.forum.SubscribeButton.commonTranslations.updateSubscriptionFailure": { + "defaultMessage": "Failed to update subscription - {error}" }, - "course.forum.FormShow.markAllAsReadSuccess": { - "defaultMessage": "All topics in this forum have been marked as read." + "course.forum.SubscribeButton.forumTopicTranslations.adminSettingSubscribed": { + "defaultMessage": "Subscription of forum topics is disabled by the course admin." }, - "course.forum.FormShow.newTopic": { - "defaultMessage": "New Topic" + "course.forum.SubscribeButton.forumTopicTranslations.subscribeSuccess": { + "defaultMessage": "You have successfully been subscribed to the forum topic {title}." }, - "course.forum.ForumEdit.editForum": { - "defaultMessage": "Edit Forum" + "course.forum.SubscribeButton.forumTopicTranslations.subscribeTooltip": { + "defaultMessage": "Subscribe to receive email notifications when someone replies in this forum topic." }, - "course.forum.ForumEdit.updateFailure": { - "defaultMessage": "Failed to update the forum." + "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeSuccess": { + "defaultMessage": "You have successfully been unsubscribed from the forum topic {title}." }, - "course.forum.ForumEdit.updateSuccess": { - "defaultMessage": "Forum {title} has been updated." + "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeTooltip": { + "defaultMessage": "Unsubscribe to stop receiving email notifications when someone replies in this forum topic." }, - "course.forum.ForumForm.description": { - "defaultMessage": "Description" + "course.forum.SubscribeButton.forumTopicTranslations.userSettingSubscribed": { + "defaultMessage": "You have unsubscribed from \"New Post and Reply\" for forums in this course. Please go to {manageMySubscriptionLink} to enable it." }, - "course.forum.ForumForm.forumTopicsAutoSubscribe": { - "defaultMessage": "Enable auto-subscription to a forum topic when a user creates the topic, new posts or replies." + "course.forum.SubscribeButton.forumTranslations.adminSettingSubscribed": { + "defaultMessage": "Subscription of new forum topic is disabled by the course admin." }, - "course.forum.ForumForm.name": { - "defaultMessage": "Name" + "course.forum.SubscribeButton.forumTranslations.subscribeSuccess": { + "defaultMessage": "You have successfully been subscribed to {title}." }, - "course.forum.ForumManagementButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete this forum \"{title}\"?" + "course.forum.SubscribeButton.forumTranslations.subscribeTooltip": { + "defaultMessage": "Subscribe to receive an email notification when a new topic is created." }, - "course.forum.ForumManagementButtons.deletionFailure": { - "defaultMessage": "Failed to delete forum - {error}" + "course.forum.SubscribeButton.forumTranslations.unsubscribeSuccess": { + "defaultMessage": "You have successfully been unsubscribed from {title}." }, - "course.forum.ForumManagementButtons.deletionSuccess": { - "defaultMessage": "Forum {title} was deleted." + "course.forum.SubscribeButton.forumTranslations.unsubscribeTooltip": { + "defaultMessage": "Unsubscribe to stop receiving email notifications when a new topic is created." }, - "course.forum.ForumNew.creationFailure": { - "defaultMessage": "Failed to create forum." + "course.forum.SubscribeButton.forumTranslations.userSettingSubscribed": { + "defaultMessage": "You have unsubscribed from \"New Topic\" for forums in this course. Please go to {manageMySubscriptionLink} to enable it." }, - "course.forum.ForumNew.creationSuccess": { - "defaultMessage": "Forum {title} has been created." + "course.forum.VotePostButton.updateFailure": { + "defaultMessage": "Failed to update the vote number - {error}" }, - "course.forum.ForumNew.newForum": { - "defaultMessage": "New Forum" + "course.forum.forum.markAllAsReadFailed": { + "defaultMessage": "Failed to mark all topics in this forum as read. Please try again later." }, - "course.forum.ForumTable.autoSubscribe": { - "defaultMessage": "Users will be automatically subscribed to a topic in this forum when they create a post in the topic." + "course.forum.publishButton.generateReplyDisabledTooltip": { + "defaultMessage": "Disabled for generated reply" }, - "course.forum.ForumTable.forum": { - "defaultMessage": "Forum" + "course.forum.publishButton.generateReplySuccess": { + "defaultMessage": "Failed to generate a reply." }, - "course.forum.ForumTable.hasUnresolved": { - "defaultMessage": "Has unresolved question(s)" + "course.forum.publishButton.generateReplyTooltip": { + "defaultMessage": "Generate a draft reply using AI" }, - "course.forum.ForumTable.isSubscribed": { - "defaultMessage": "Subscribed?" + "course.forum.publishButton.publish": { + "defaultMessage": "Publish" }, - "course.forum.ForumTable.noForum": { - "defaultMessage": "No Forum" + "course.forum.publishButton.publishFailure": { + "defaultMessage": "Failed to publish the post." }, - "course.forum.ForumTable.posts": { - "defaultMessage": "Posts" + "course.forum.publishButton.publishSuccess": { + "defaultMessage": "The post has succesfully been published." }, - "course.forum.ForumTable.topics": { - "defaultMessage": "Topics" + "course.forum.publishButton.publishTooltip": { + "defaultMessage": "Pusblish post to students" }, - "course.forum.ForumTable.views": { - "defaultMessage": "Views" + "course.gradebook.AddExternalColumnPrompt.cancel": { + "defaultMessage": "Cancel" }, - "course.forum.ForumTable.votes": { - "defaultMessage": "Votes" + "course.gradebook.AddExternalColumnPrompt.create": { + "defaultMessage": "Create" }, - "course.forum.ForumTopicEdit.editForum": { - "defaultMessage": "Edit Topic" + "course.gradebook.AddExternalColumnPrompt.error": { + "defaultMessage": "Could not create the external assessment." }, - "course.forum.ForumTopicEdit.updateFailure": { - "defaultMessage": "Failed to update the topic." + "course.gradebook.AddExternalColumnPrompt.maxLabel": { + "defaultMessage": "Max marks" }, - "course.forum.ForumTopicEdit.updateSuccess": { - "defaultMessage": "Topic {title} has been updated." + "course.gradebook.AddExternalColumnPrompt.nameLabel": { + "defaultMessage": "Name" }, - "course.forum.ForumTopicForm.postAnonymously": { - "defaultMessage": "Anonymous post" + "course.gradebook.AddExternalColumnPrompt.success": { + "defaultMessage": "External assessment created." }, - "course.forum.ForumTopicForm.text": { - "defaultMessage": "Text" + "course.gradebook.AddExternalColumnPrompt.title": { + "defaultMessage": "Add external assessment" + }, + "course.gradebook.ConfigureWeightsPrompt.allExcluded": { + "defaultMessage": "All assessments in \"{tab}\" are excluded — it contributes nothing to the total." }, - "course.forum.ForumTopicForm.title": { - "defaultMessage": "Title" + "course.gradebook.ConfigureWeightsPrompt.allExcludedCount": { + "defaultMessage": "All {n} excluded" }, - "course.forum.ForumTopicForm.topicType": { - "defaultMessage": "Topic Type" + "course.gradebook.ConfigureWeightsPrompt.customMode": { + "defaultMessage": "Custom" }, - "course.forum.ForumTopicForm.topicType.announcement": { - "defaultMessage": "Announcement" + "course.gradebook.ConfigureWeightsPrompt.customSum": { + "defaultMessage": "Assessment weights: {sum} / {total}" }, - "course.forum.ForumTopicForm.topicType.normal": { - "defaultMessage": "Normal" + "course.gradebook.ConfigureWeightsPrompt.defaultsHint": { + "defaultMessage": "No weights set yet — these are suggested defaults with every tab counting equally. Save to confirm, or adjust below." }, - "course.forum.ForumTopicForm.topicType.question": { - "defaultMessage": "Question" + "course.gradebook.ConfigureWeightsPrompt.descriptionDrop": { + "defaultMessage": "In Equal mode, optionally drop each student's N lowest-scoring assessments before averaging." }, - "course.forum.ForumTopicForm.topicType.sticky": { - "defaultMessage": "Sticky" + "course.gradebook.ConfigureWeightsPrompt.descriptionExclusion": { + "defaultMessage": "Expand a tab to include or exclude individual assessments from grading." }, - "course.forum.ForumTopicManagementButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete this topic \"{title}\"?" + "course.gradebook.ConfigureWeightsPrompt.descriptionIntro": { + "defaultMessage": "Control how tabs and assessments count toward each student's total grade." }, - "course.forum.ForumTopicManagementButtons.deletionFailure": { - "defaultMessage": "Failed to delete topic - {error}" + "course.gradebook.ConfigureWeightsPrompt.descriptionModes": { + "defaultMessage": "Choose Equal (all assessments share the tab's weight) or Custom (set each assessment's share)." }, - "course.forum.ForumTopicManagementButtons.deletionSuccess": { - "defaultMessage": "Topic {title} was deleted." + "course.gradebook.ConfigureWeightsPrompt.descriptionWeights": { + "defaultMessage": "Set each tab's weight — how much it contributes to the total (weights should sum to 100)." }, - "course.forum.ForumTopicNew.creationFailure": { - "defaultMessage": "Failed to create topic." + "course.gradebook.ConfigureWeightsPrompt.equalMode": { + "defaultMessage": "Equal" }, - "course.forum.ForumTopicNew.creationSuccess": { - "defaultMessage": "Topic {title} has been created." + "course.gradebook.ConfigureWeightsPrompt.excluded": { + "defaultMessage": "Excluded" }, - "course.forum.ForumTopicNew.newTopic": { - "defaultMessage": "New Topic" + "course.gradebook.ConfigureWeightsPrompt.excludedCount": { + "defaultMessage": "{n} excluded" }, - "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptAction": { - "defaultMessage": "Discard" + "course.gradebook.ConfigureWeightsPrompt.includeAssessment": { + "defaultMessage": "Include {assessment} in grade" }, - "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptMessage": { - "defaultMessage": "You have edited this post and there are unsaved changes. Do you wish to proceed?" + "course.gradebook.ConfigureWeightsPrompt.modeAria": { + "defaultMessage": "{tab} weight mode" }, - "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptTitle": { - "defaultMessage": "Discard unsaved changes?" + "course.gradebook.ConfigureWeightsPrompt.ofGrade": { + "defaultMessage": "{pct}% of grade" }, - "course.forum.ForumTopicPostEditActionButtons.emptyPost": { - "defaultMessage": "Post cannot be empty!" + "course.gradebook.ConfigureWeightsPrompt.promptTitle": { + "defaultMessage": "Configure contributions" }, - "course.forum.ForumTopicPostEditActionButtons.updateFailure": { - "defaultMessage": "Failed to update the post - {error}" + "course.gradebook.ConfigureWeightsPrompt.saveError": { + "defaultMessage": "Failed to save weights. Please try again." }, - "course.forum.ForumTopicPostEditActionButtons.updateSuccess": { - "defaultMessage": "The post has been updated." + "course.gradebook.ConfigureWeightsPrompt.total": { + "defaultMessage": "Total: {sum}%" }, - "course.forum.ForumTopicPostForm.postAnonymously": { - "defaultMessage": "Anonymous post" + "course.gradebook.ConfigureWeightsPrompt.unbalanced": { + "defaultMessage": "Assessment weights for \"{tab}\" must sum to its tab total before saving." }, - "course.forum.ForumTopicPostManagementButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete this topic post?" + "course.gradebook.ConfigureWeightsPrompt.valueTooHigh": { + "defaultMessage": "Value must be at most 100" }, - "course.forum.ForumTopicPostManagementButtons.deletionFailure": { - "defaultMessage": "Failed to delete topic - {error}" + "course.gradebook.ConfigureWeightsPrompt.valueTooLow": { + "defaultMessage": "Value must be at least 0" }, - "course.forum.ForumTopicPostManagementButtons.deletionSuccess": { - "defaultMessage": "The post has been deleted." + "course.gradebook.ConfigureWeightsPrompt.weightsDoNotSum": { + "defaultMessage": "Weights do not sum to 100. Saving is allowed; Total may be inaccurate." }, - "course.forum.ForumTopicPostNew.creationFailure": { - "defaultMessage": "Failed to create the post - {error}" + "course.gradebook.DeleteExternalColumnPrompt.body": { + "defaultMessage": "Delete \"{title}\"? This permanently removes the column and every student grade in it. This cannot be undone." }, - "course.forum.ForumTopicPostNew.creationSuccess": { - "defaultMessage": "The post has been created." + "course.gradebook.DeleteExternalColumnPrompt.cancel": { + "defaultMessage": "Cancel" }, - "course.forum.ForumTopicPostNew.newPost": { - "defaultMessage": "Create a New Post" + "course.gradebook.DeleteExternalColumnPrompt.confirm": { + "defaultMessage": "Delete" }, - "course.forum.ForumTopicShow.fetchPostsFailure": { - "defaultMessage": "Failed to retrieve forum topic data." + "course.gradebook.DeleteExternalColumnPrompt.error": { + "defaultMessage": "Could not delete the external assessment." }, - "course.forum.ForumTopicShow.header": { - "defaultMessage": "Forum Topic Posts" + "course.gradebook.DeleteExternalColumnPrompt.title": { + "defaultMessage": "Delete external assessment" }, - "course.forum.ForumTopicShow.lockedNote": { - "defaultMessage": "You are unable to add new post as this topic has been locked by the teaching staff." + "course.gradebook.ExternalGradeConflictPrompt.body": { + "defaultMessage": "These students already have a grade for these components. Keep their existing grades, or replace them with the values from your file? New students and blank cells are unaffected." }, - "course.forum.ForumTopicShow.noPosts": { - "defaultMessage": "No Post" + "course.gradebook.ExternalGradeConflictPrompt.goBack": { + "defaultMessage": "Go Back" }, - "course.forum.ForumTopicShow.topicResolved": { - "defaultMessage": "This question topic has been resolved." + "course.gradebook.ExternalGradeConflictPrompt.keepExisting": { + "defaultMessage": "Keep Existing" }, - "course.forum.ForumTopicShow.topicUnresolved": { - "defaultMessage": "This question topic is unresolved." + "course.gradebook.ExternalGradeConflictPrompt.replace": { + "defaultMessage": "Replace" }, - "course.forum.ForumTopicShow.topicUnresolvedNote": { - "defaultMessage": "Mark helpful post(s) as answer(s) to resolve this question." + "course.gradebook.ExternalGradeConflictPrompt.title": { + "defaultMessage": "Resolve grade conflicts" }, - "course.forum.ForumTopicTable.hidden": { - "defaultMessage": "This topic is hidden for students." + "course.gradebook.ExternalGradeConflictTable.component": { + "defaultMessage": "Component" }, - "course.forum.ForumTopicTable.isSubscribed": { - "defaultMessage": "Subscribed?" + "course.gradebook.ExternalGradeConflictTable.existing": { + "defaultMessage": "Existing grade" }, - "course.forum.ForumTopicTable.lastPostedBy": { - "defaultMessage": "Last Posted By" + "course.gradebook.ExternalGradeConflictTable.inFile": { + "defaultMessage": "In-file grade" }, - "course.forum.ForumTopicTable.locked": { - "defaultMessage": "This topic is closed; it no longer accepts new replies." + "course.gradebook.ExternalGradeConflictTable.mismatch": { + "defaultMessage": "This identifier now resolves to a different student than the existing grade was imported under." }, - "course.forum.ForumTopicTable.noTopic": { - "defaultMessage": "No Topic" + "course.gradebook.ExternalGradeConflictTable.student": { + "defaultMessage": "Student" }, - "course.forum.ForumTopicTable.posts": { - "defaultMessage": "Posts" + "course.gradebook.GradeLinkHint.hint": { + "defaultMessage": "Each grade is the total of the marks in a student's submission. Click any grade to open that submission and adjust the marks." }, - "course.forum.ForumTopicTable.resolved": { - "defaultMessage": "Question (Resolved)" + "course.gradebook.GradebookColumnTree.alwaysIncluded": { + "defaultMessage": "Always included" }, - "course.forum.ForumTopicTable.startedBy": { - "defaultMessage": "Started By" + "course.gradebook.GradebookColumnTree.gamification": { + "defaultMessage": "Gamification" }, - "course.forum.ForumTopicTable.topics": { - "defaultMessage": "Topics" + "course.gradebook.GradebookColumnTree.grades": { + "defaultMessage": "Grades" }, - "course.forum.ForumTopicTable.unresolved": { - "defaultMessage": "Question (Unresolved)" + "course.gradebook.GradebookColumnTree.studentInfo": { + "defaultMessage": "Student info" }, - "course.forum.ForumTopicTable.views": { - "defaultMessage": "Views" + "course.gradebook.GradebookIndex.addExternal": { + "defaultMessage": "Add external assessment" }, - "course.forum.ForumTopicTable.votes": { - "defaultMessage": "Votes" + "course.gradebook.GradebookIndex.allAssessments": { + "defaultMessage": "All assessments" }, - "course.forum.ForumsIndex.fetchForumsFailure": { - "defaultMessage": "Failed to retrieve forum data." + "course.gradebook.GradebookIndex.byWeight": { + "defaultMessage": "Weighted total" }, - "course.forum.ForumsIndex.header": { - "defaultMessage": "Forums" + "course.gradebook.GradebookIndex.dialogTitle": { + "defaultMessage": "Select columns" }, - "course.forum.ForumsIndex.markAllAsReadFailed": { - "defaultMessage": "Failed to mark all topics as read. Please try again later." + "course.gradebook.GradebookIndex.exportAllTooltip": { + "defaultMessage": "No rows selected - all rows will be exported." }, - "course.forum.ForumsIndex.markAllAsReadSuccess": { - "defaultMessage": "All topics have been marked as read." + "course.gradebook.GradebookIndex.exportButton": { + "defaultMessage": "Export all rows" }, - "course.forum.ForumsIndex.newForum": { - "defaultMessage": "New Forum" + "course.gradebook.GradebookIndex.exportRows": { + "defaultMessage": "Export {count, plural, one {# row} other {# rows}}" }, - "course.forum.HideButton.hide": { - "defaultMessage": "Hide" + "course.gradebook.GradebookIndex.fetchFailure": { + "defaultMessage": "Failed to retrieve Gradebook." }, - "course.forum.HideButton.hideTooltip": { - "defaultMessage": "Hide topic from students" + "course.gradebook.GradebookIndex.gradebook": { + "defaultMessage": "Gradebook" }, - "course.forum.HideButton.hideFailure": { - "defaultMessage": "Failed to hide the topic \"{title}\" - {error}" + "course.gradebook.GradebookIndex.noStudents": { + "defaultMessage": "No students enrolled yet" }, - "course.forum.HideButton.hideSuccess": { - "defaultMessage": "The topic \"{title}\" has successfully been hidden." + "course.gradebook.GradebookIndex.noStudentsHint": { + "defaultMessage": "Grades will appear here once students join the course." }, - "course.forum.HideButton.unhide": { - "defaultMessage": "Unhide" + "course.gradebook.GradebookIndex.searchStudents": { + "defaultMessage": "Search students" }, - "course.forum.HideButton.unhideTooltip": { - "defaultMessage": "Show topic to students" + "course.gradebook.GradebookIndex.selectColumns": { + "defaultMessage": "Select Columns" }, - "course.forum.HideButton.unhideFailure": { - "defaultMessage": "Failed to unhide the topic \"{title}\" - {error}" + "course.gradebook.GradebookTable.delete": { + "defaultMessage": "Delete" }, - "course.forum.HideButton.unhideSuccess": { - "defaultMessage": "The topic \"{title}\" has successfully been unhidden." + "course.gradebook.GradebookTable.externalBadge": { + "defaultMessage": "External" }, - "course.forum.LockButton.locked": { - "defaultMessage": "Lock" + "course.gradebook.GradebookTable.externalGradeAria": { + "defaultMessage": "{title} grade for {name}" }, - "course.forum.LockButton.lockTooltip": { - "defaultMessage": "Lock to stop students from posting in this topic" + "course.gradebook.GradebookTable.externalMaxAria": { + "defaultMessage": "{title} max marks" }, - "course.forum.LockButton.lockedFailure": { - "defaultMessage": "Failed to locked the topic \"{title}\" - {error}" + "course.gradebook.GradebookTable.gradeSaveError": { + "defaultMessage": "Could not save the grade. Please try again." }, - "course.forum.LockButton.lockedSuccess": { - "defaultMessage": "The topic \"{title}\" has successfully been locked." + "course.gradebook.GradebookTable.manageAria": { + "defaultMessage": "manage {title}" }, - "course.forum.LockButton.unlocked": { - "defaultMessage": "Unlock" + "course.gradebook.GradebookTable.maxMarks": { + "defaultMessage": "Max Marks" }, - "course.forum.LockButton.unlockTooltip": { - "defaultMessage": "Unlock to allow students to post within this topic" + "course.gradebook.GradebookTable.maxSaveError": { + "defaultMessage": "Could not save the max marks. Please try again." }, - "course.forum.LockButton.unlockedFailure": { - "defaultMessage": "Failed to unlocked the topic \"{title}\" - {error}" + "course.gradebook.GradebookTable.noDataColumnsHint": { + "defaultMessage": "No grade columns selected - export will include student info only." }, - "course.forum.LockButton.unlockedSuccess": { - "defaultMessage": "The topic \"{title}\" has successfully been unlocked." + "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { + "defaultMessage": "No grade or gamification columns selected - export will include student info only." }, - "course.forum.MarkAllAsReadButton.AllReadTooltip": { - "defaultMessage": "Hooray! All topics have been read!" + "course.gradebook.GradebookTable.rename": { + "defaultMessage": "Rename" }, - "course.forum.MarkAllAsReadButton.markAllAsRead": { - "defaultMessage": "Mark all as read" + "course.gradebook.GradebookWeightedTable.collapseRow": { + "defaultMessage": "Collapse {name}" }, - "course.forum.MarkAllAsReadButton.markAllAsReadTooltip": { - "defaultMessage": "Mark all forum posts on the current page as read" + "course.gradebook.GradebookWeightedTable.configureWeights": { + "defaultMessage": "Configure Weights" }, - "course.forum.MarkAnswerButton.markAsAnswer": { - "defaultMessage": "Mark as answer" + "course.gradebook.GradebookWeightedTable.defaultWeights": { + "defaultMessage": "Showing default weights — every tab counts equally. Click \"Configure Weights\" to set your own." }, - "course.forum.MarkAnswerButton.markedAsAnswer": { - "defaultMessage": "Marked as answer" + "course.gradebook.GradebookWeightedTable.defaultWeightsNoAccess": { + "defaultMessage": "Showing default weights — every tab counts equally until weights are configured." }, - "course.forum.MarkAnswerButton.unmarkAsAnswer": { - "defaultMessage": "Unmark as answer" + "course.gradebook.GradebookWeightedTable.displayMode": { + "defaultMessage": "Display mode" }, - "course.forum.MarkAnswerButton.updateFailure": { - "defaultMessage": "Failed to update the post - {error}" + "course.gradebook.GradebookWeightedTable.displayPercent": { + "defaultMessage": "Percentage" }, - "course.forum.NextUnreadButton.AllReadTooltip": { - "defaultMessage": "Hooray! All topics have been read!" + "course.gradebook.GradebookWeightedTable.displayPercentTooltip": { + "defaultMessage": "What fraction of each tab the student earned. 100% on a tab worth 20% = the student earned all 20 grade points from that tab." }, - "course.forum.NextUnreadButton.nextUnread": { - "defaultMessage": "Next Unread" + "course.gradebook.GradebookWeightedTable.displayPoints": { + "defaultMessage": "Points" }, - "course.forum.NextUnreadButton.nextUnreadTooltip": { - "defaultMessage": "Jump to next unread topic" + "course.gradebook.GradebookWeightedTable.displayPointsTooltip": { + "defaultMessage": "How many grade points each tab contributes. Columns add up to the projected total." }, - "course.forum.PostCreatorObject.anonymousUser": { - "defaultMessage": "Anonymous User" + "course.gradebook.GradebookWeightedTable.downloadCsv": { + "defaultMessage": "Download as CSV" }, - "course.forum.PostCreatorObject.maskUser": { - "defaultMessage": "Mask User" + "course.gradebook.GradebookWeightedTable.excluded": { + "defaultMessage": "Excluded" }, - "course.forum.PostCreatorObject.postAnonymously": { - "defaultMessage": "Anonymous post" + "course.gradebook.GradebookWeightedTable.expandRow": { + "defaultMessage": "Expand {name}" }, - "course.forum.PostCreatorObject.unmaskUser": { - "defaultMessage": "Unmask User" + "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { + "defaultMessage": "No weights configured — all tab weights are 0. Click \"Configure Weights\" to assign weights." }, - "course.forum.ReplyCard.emptyPost": { - "defaultMessage": "Post cannot be empty!" + "course.gradebook.GradebookWeightedTable.noWeightsNoAccess": { + "defaultMessage": "No tab weights have been configured yet." }, - "course.forum.ReplyCard.postAnonymously": { - "defaultMessage": "Anonymous post" + "course.gradebook.GradebookWeightedTable.outOfWeight": { + "defaultMessage": "/{weight}" }, - "course.forum.ReplyCard.replyFailure": { - "defaultMessage": "Failed to submit the post - {error}" + "course.gradebook.GradebookWeightedTable.percentOfGrade": { + "defaultMessage": "{weight}% of grade" }, - "course.forum.ReplyCard.replySuccess": { - "defaultMessage": "The reply post has been created." + "course.gradebook.GradebookWeightedTable.percentTotalExact": { + "defaultMessage": "100% total" }, - "course.forum.ReplyCard.replyTo": { - "defaultMessage": "Reply to {user}" + "course.gradebook.GradebookWeightedTable.percentTotalWarning": { + "defaultMessage": "{weight}% total" }, - "course.forum.SubscribeButton.commonTranslations.manageMySubscription": { - "defaultMessage": "Manage My Subscriptions" + "course.gradebook.GradebookWeightedTable.searchStudents": { + "defaultMessage": "Search students" }, - "course.forum.SubscribeButton.commonTranslations.subscribe": { - "defaultMessage": "Subscribe" + "course.gradebook.GradebookWeightedTable.total": { + "defaultMessage": "Total" }, - "course.forum.SubscribeButton.commonTranslations.unsubscribe": { - "defaultMessage": "Unsubscribe" + "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { + "defaultMessage": "Weights do not sum to 100. Total may be inaccurate." }, - "course.forum.SubscribeButton.commonTranslations.updateSubscriptionFailure": { - "defaultMessage": "Failed to update subscription - {error}" + "course.gradebook.ImportExternalAssessmentsButton.label": { + "defaultMessage": "Import external assessments" }, - "course.forum.SubscribeButton.forumTopicTranslations.adminSettingSubscribed": { - "defaultMessage": "Subscription of forum topics is disabled by the course admin." + "course.gradebook.ImportWizard.addComponent": { + "defaultMessage": "Add component" }, - "course.forum.SubscribeButton.forumTopicTranslations.subscribeSuccess": { - "defaultMessage": "You have successfully been subscribed to the forum topic {title}." + "course.gradebook.ImportWizard.back": { + "defaultMessage": "Back" }, - "course.forum.SubscribeButton.forumTopicTranslations.subscribeTooltip": { - "defaultMessage": "Subscribe to receive email notifications when someone replies in this forum topic." + "course.gradebook.ImportWizard.cancel": { + "defaultMessage": "Cancel" }, - "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeSuccess": { - "defaultMessage": "You have successfully been unsubscribed from the forum topic {title}." + "course.gradebook.ImportWizard.commitError": { + "defaultMessage": "Import failed. Nothing was saved." }, - "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeTooltip": { - "defaultMessage": "Unsubscribe to stop receiving email notifications when someone replies in this forum topic." + "course.gradebook.ImportWizard.committed": { + "defaultMessage": "Import complete." }, - "course.forum.SubscribeButton.forumTopicTranslations.userSettingSubscribed": { - "defaultMessage": "You have unsubscribed from \"New Post and Reply\" for forums in this course. Please go to {manageMySubscriptionLink} to enable it." + "course.gradebook.ImportWizard.componentName": { + "defaultMessage": "Component name" }, - "course.forum.SubscribeButton.forumTranslations.adminSettingSubscribed": { - "defaultMessage": "Subscription of new forum topic is disabled by the course admin." + "course.gradebook.ImportWizard.continue": { + "defaultMessage": "Confirm import" }, - "course.forum.SubscribeButton.forumTranslations.subscribeSuccess": { - "defaultMessage": "You have successfully been subscribed to {title}." + "course.gradebook.ImportWizard.downloadTemplate": { + "defaultMessage": "Download template" }, - "course.forum.SubscribeButton.forumTranslations.subscribeTooltip": { - "defaultMessage": "Subscribe to receive an email notification when a new topic is created." + "course.gradebook.ImportWizard.email": { + "defaultMessage": "Email" }, - "course.forum.SubscribeButton.forumTranslations.unsubscribeSuccess": { - "defaultMessage": "You have successfully been unsubscribed from {title}." + "course.gradebook.ImportWizard.identifierMode": { + "defaultMessage": "Match students by" }, - "course.forum.SubscribeButton.forumTranslations.unsubscribeTooltip": { - "defaultMessage": "Unsubscribe to stop receiving email notifications when a new topic is created." + "course.gradebook.ImportWizard.malformed": { + "defaultMessage": "These cells are not valid numbers: {cells}" }, - "course.forum.SubscribeButton.forumTranslations.userSettingSubscribed": { - "defaultMessage": "You have unsubscribed from \"New Topic\" for forums in this course. Please go to {manageMySubscriptionLink} to enable it." + "course.gradebook.ImportWizard.maxMarks": { + "defaultMessage": "Max marks" }, - "course.forum.VotePostButton.updateFailure": { - "defaultMessage": "Failed to update the vote number - {error}" + "course.gradebook.ImportWizard.next": { + "defaultMessage": "Next" }, - "course.forum.forum.markAllAsReadFailed": { - "defaultMessage": "Failed to mark all topics in this forum as read. Please try again later." + "course.gradebook.ImportWizard.previewError": { + "defaultMessage": "Could not verify the file. Please try again." }, - "course.gradebook.GradebookColumnTree.grades": { - "defaultMessage": "Grades" + "course.gradebook.ImportWizard.stepDefine": { + "defaultMessage": "Define components" }, - "course.gradebook.GradebookColumnTree.alwaysIncluded": { - "defaultMessage": "Always included" + "course.gradebook.ImportWizard.stepUpload": { + "defaultMessage": "Template & upload" }, - "course.gradebook.GradebookColumnTree.studentInfo": { - "defaultMessage": "Student info" + "course.gradebook.ImportWizard.stepVerify": { + "defaultMessage": "Verify" }, - "course.gradebook.GradebookColumnTree.gamification": { - "defaultMessage": "Gamification" + "course.gradebook.ImportWizard.studentId": { + "defaultMessage": "Student ID" }, - "course.gradebook.GradebookIndex.dialogTitle": { - "defaultMessage": "Select columns" + "course.gradebook.ImportWizard.studentIdHint": { + "defaultMessage": "Matching uses each student's current Student ID. Keep Student IDs up to date in Manage Users." }, - "course.gradebook.GradebookIndex.exportAllTooltip": { - "defaultMessage": "No rows selected - all rows will be exported." + "course.gradebook.ImportWizard.title": { + "defaultMessage": "Import external assessments" }, - "course.gradebook.GradebookIndex.exportButton": { - "defaultMessage": "Export all rows" + "course.gradebook.ImportWizard.unresolved": { + "defaultMessage": "These identifiers were not found in the course: {ids}" }, - "course.gradebook.GradebookIndex.exportRows": { - "defaultMessage": "Export {count, plural, one {# row} other {# rows}}" + "course.gradebook.ImportWizard.updatesExisting": { + "defaultMessage": "Updates existing — managed in the gradebook" }, - "course.gradebook.GradebookIndex.selectColumns": { - "defaultMessage": "Select Columns" + "course.gradebook.ImportWizard.upload": { + "defaultMessage": "Upload filled CSV" }, - "course.gradebook.GradebookIndex.applyAndExport": { - "defaultMessage": "Apply and Export" + "course.gradebook.ImportWizard.verify": { + "defaultMessage": "Verify" }, - "course.gradebook.GradebookIndex.fetchFailure": { - "defaultMessage": "Failed to retrieve Gradebook." + "course.gradebook.ImportWizard.weightage": { + "defaultMessage": "Weightage" }, - "course.gradebook.GradebookIndex.gradebook": { - "defaultMessage": "Gradebook" + "course.gradebook.ProjectedTotalHint.policy": { + "defaultMessage": "Totals count ungraded assessments as 0." }, - "course.gradebook.GradebookIndex.searchStudents": { - "defaultMessage": "Search students" + "course.gradebook.RenameExternalColumnPrompt.cancel": { + "defaultMessage": "Cancel" }, - "course.gradebook.GradebookIndex.noStudents": { - "defaultMessage": "No students enrolled yet" + "course.gradebook.RenameExternalColumnPrompt.error": { + "defaultMessage": "Could not rename the external assessment." }, - "course.gradebook.GradebookIndex.noStudentsHint": { - "defaultMessage": "Grades will appear here once students join the course." + "course.gradebook.RenameExternalColumnPrompt.nameLabel": { + "defaultMessage": "Name" }, - "course.gradebook.GradebookTable.maxMarks": { - "defaultMessage": "Max Marks" + "course.gradebook.RenameExternalColumnPrompt.save": { + "defaultMessage": "Save" }, - "course.gradebook.GradebookTable.noDataColumnsHint": { - "defaultMessage": "No grade columns selected - export will include student info only." + "course.gradebook.RenameExternalColumnPrompt.title": { + "defaultMessage": "Rename external assessment" }, - "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { - "defaultMessage": "No grade or gamification columns selected - export will include student info only." + "course.gradebook.WeightedViewHint.hint": { + "defaultMessage": "Want a weighted total grade? You can set how much each tab counts toward each student’s overall grade and view the weighted total here. Turn it on in {link}." }, - "course.gradebook.GradeLinkHint.hint": { - "defaultMessage": "Each grade is the total of the marks in a student's submission. Click any grade to open that submission and adjust the marks." + "course.gradebook.WeightedViewHint.settingsLink": { + "defaultMessage": "Gradebook settings" }, "course.group.GroupCreationForm.description": { "defaultMessage": "Description (Optional)" @@ -5705,27 +6797,27 @@ "course.leaderboard.LeaderboardTable.average": { "defaultMessage": "Average" }, - "course.leaderboard.LeaderboardTable.experience": { - "defaultMessage": "Experience" + "course.leaderboard.LeaderboardTable.averageAchievements": { + "defaultMessage": "Average Achievements" }, - "course.leaderboard.LeaderboardTable.rank": { - "defaultMessage": "Rank" + "course.leaderboard.LeaderboardTable.averageExperience": { + "defaultMessage": "Average Experience" }, - "course.leaderboard.LeaderboardTable.name": { - "defaultMessage": "Name" + "course.leaderboard.LeaderboardTable.experience": { + "defaultMessage": "Experience" }, "course.leaderboard.LeaderboardTable.level": { "defaultMessage": "Level" }, - "course.leaderboard.LeaderboardTable.averageExperience": { - "defaultMessage": "Average Experience" - }, - "course.leaderboard.LeaderboardTable.averageAchievements": { - "defaultMessage": "Average Achievements" - }, "course.leaderboard.LeaderboardTable.members": { "defaultMessage": "Members" }, + "course.leaderboard.LeaderboardTable.name": { + "defaultMessage": "Name" + }, + "course.leaderboard.LeaderboardTable.rank": { + "defaultMessage": "Rank" + }, "course.leaderboard.LeaderboardTable.titleAchievements": { "defaultMessage": "By Achievements" }, @@ -5894,20 +6986,56 @@ "course.level.Level.levelHeader": { "defaultMessage": "Levels" }, - "course.level.Level.saveFailure": { - "defaultMessage": "Level saving failed, please try again." + "course.level.Level.orderedIncorrectly": { + "defaultMessage": "Levels will be sorted automatically when saved regardless of their order here." + }, + "course.level.Level.placeholder": { + "defaultMessage": "0" + }, + "course.level.Level.reset": { + "defaultMessage": "Reset" + }, + "course.level.Level.resetTooltip": { + "defaultMessage": "Reset changes" + }, + "course.level.Level.saveChanges": { + "defaultMessage": "Save" }, - "course.level.Level.saveLevels": { - "defaultMessage": "Save Levels" + "course.level.Level.saveFailure": { + "defaultMessage": "Failed to save levels" }, "course.level.Level.saveSuccess": { "defaultMessage": "Levels Saved" }, "course.level.Level.thresholdHeader": { - "defaultMessage": "Threshold" + "defaultMessage": "EXP Threshold" + }, + "course.level.Level.unsavedChanges": { + "defaultMessage": "You have unsaved changes" + }, + "course.material.files.DownloadingFilePage.clickToDownloadFile": { + "defaultMessage": "Download {name}" + }, + "course.material.files.DownloadingFilePage.clickToDownloadFileDescription": { + "defaultMessage": "Something happened when initiating an automatic download. Click the link below to immediately download the file." + }, + "course.material.files.DownloadingFilePage.downloading": { + "defaultMessage": "Downloading {name}" + }, + "course.material.files.DownloadingFilePage.downloadingDescription": { + "defaultMessage": "This file should start downloading automatically now. If it doesn't, you can try again by clicking the link below or refreshing this page." + }, + "course.material.files.DownloadingFilePage.tryDownloadingAgain": { + "defaultMessage": "Try downloading again" + }, + "course.material.files.ErrorRetrievingFilePage.goToTheWorkbin": { + "defaultMessage": "Go to the Workbin" }, - "course.level.LevelRow.zeroThresholdError": { - "defaultMessage": "Experience points threshold cannot be 0" + "course.material.files.ErrorRetrievingFilePage.problemRetrievingFile": { + "defaultMessage": "Problem retrieving file" + }, + "course.material.files.ErrorRetrievingFilePage.problemRetrievingFileDescription": { + "defaultMessage": "Either it no longer exists, you don't have the permission to access it, or something unexpected happened when we were trying to retrieve it." }, "course.material.folders.DownloadFolderButton.downloadFolderErrorMessage": { "defaultMessage": "Download has failed. Please try again later." @@ -5918,6 +7046,15 @@ "course.material.folders.DownloadFolderButton.downloading": { "defaultMessage": "Downloading..." }, + "course.material.folders.ErrorRetrievingFolderPage.goToMainFolder": { + "defaultMessage": "Go to the main folder" + }, + "course.material.folders.ErrorRetrievingFolderPage.problemRetrievingFolder": { + "defaultMessage": "Problem retrieving folder" + }, + "course.material.folders.ErrorRetrievingFolderPage.problemRetrievingFolderDescription": { + "defaultMessage": "Either it no longer exists, you don't have the permission to access it, or something unexpected happened when we were trying to retrieve it." + }, "course.material.folders.FolderEdit.editSubfolderTitle": { "defaultMessage": "Edit Folder" }, @@ -5957,6 +7094,12 @@ "course.material.folders.FolderShow.defaultHeader": { "defaultMessage": "Materials" }, + "course.material.folders.FolderShow.error": { + "defaultMessage": "(Error)" + }, + "course.material.folders.FolderShow.folderNotFound": { + "defaultMessage": "Folder not found" + }, "course.material.folders.MaterialEdit.editMaterialTitle": { "defaultMessage": "Edit Material" }, @@ -6002,41 +7145,71 @@ "course.material.folders.UploadFilesButton.uploadFilesTooltip": { "defaultMessage": "Upload" }, + "course.material.folders.WorkbinTable.lastModified": { + "defaultMessage": "Last Modified" + }, + "course.material.folders.WorkbinTable.name": { + "defaultMessage": "Name" + }, + "course.material.folders.WorkbinTable.startAt": { + "defaultMessage": "Start At" + }, "course.material.folders.WorkbinTableButtons.DeletionFailure": { "defaultMessage": "could not be deleted" }, + "course.material.folders.WorkbinTableButtons.addFailure": { + "defaultMessage": "{material} could not be added to knowledge base" + }, "course.material.folders.WorkbinTableButtons.deleteConfirmation": { "defaultMessage": "Are you sure you want to delete" }, "course.material.folders.WorkbinTableButtons.deletionSuccess": { "defaultMessage": "has been deleted" }, + "course.material.folders.WorkbinTableButtons.removeFailure": { + "defaultMessage": "{material} could not be removed from knowledge base" + }, + "course.material.folders.WorkbinTableButtons.removeSuccess": { + "defaultMessage": "{material} has been removed from knowledge base" + }, "course.material.folders.WorkbinTableButtons.tableButtonDeleteTooltip": { "defaultMessage": "Delete" }, - "course.material.folders.WorkbinTable.name": { - "defaultMessage": "Name" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.linkAssessments": { + "defaultMessage": "Link Assessments" }, - "course.material.folders.WorkbinTable.lastModified": { - "defaultMessage": "Last Modified" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.linkedAssessments": { + "defaultMessage": "Linked Assessments" }, - "course.material.folders.WorkbinTable.startAt": { - "defaultMessage": "Start At" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.searchPlaceholder": { + "defaultMessage": "Search by Assessment Title" }, - "course.plagiarism.PlagiarismIndex.header.plagiarism": { - "defaultMessage": "Plagiarism Check" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.unlinkedAssessments": { + "defaultMessage": "Available Assessments" }, - "course.plagiarism.PlagiarismIndex.assessments.assessment": { - "defaultMessage": "Assessments" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.updateLinksFailure": { + "defaultMessage": "Failed to update assessment links" }, - "course.plagiarism.PlagiarismIndex.assessments.numSubmitted": { - "defaultMessage": "# Submissions" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkDialog.updateLinksSuccess": { + "defaultMessage": "Assessment links updated successfully" }, - "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { - "defaultMessage": "# Checkable Questions" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkList.cannotManage": { + "defaultMessage": "You do not have permission to manage this assessment." }, - "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { - "defaultMessage": "Last Submission At" + "course.plagiarism.PlagiarismIndex.assessments.AssessmentLinkList.noAssessmentsFound": { + "defaultMessage": "No assessments found" + }, + "course.plagiarism.PlagiarismIndex.assessments.actions": { + "defaultMessage": "Actions" + }, + "course.plagiarism.PlagiarismIndex.assessments.assessment": { + "defaultMessage": "Assessment" + }, + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { + "defaultMessage": "Some of the selected assessments already have completed plagiarism checks. Running a new plagiarism check will remove the previous results." + }, + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { + "defaultMessage": "Confirm Plagiarism Check?" }, "course.plagiarism.PlagiarismIndex.assessments.lastRunStatus": { "defaultMessage": "Status" @@ -6044,72 +7217,117 @@ "course.plagiarism.PlagiarismIndex.assessments.lastRunTime": { "defaultMessage": "Last Run At" }, + "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { + "defaultMessage": "Last Submission At" + }, + "course.plagiarism.PlagiarismIndex.assessments.linkAssessments": { + "defaultMessage": "Link Assessments" + }, + "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { + "defaultMessage": "New submissions detected since last plagiarism run" + }, + "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { + "defaultMessage": "No new submissions since last plagiarism run" + }, + "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { + "defaultMessage": "No checkable questions" + }, + "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { + "defaultMessage": "Not enough submissions" + }, + "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { + "defaultMessage": "# Checkable Questions" + }, + "course.plagiarism.PlagiarismIndex.assessments.numSubmitted": { + "defaultMessage": "# Submissions" + }, + "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { + "defaultMessage": "New Plagiarism Check ({count})" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { + "defaultMessage": "Run Plagiarism Check" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { + "defaultMessage": "Failed to start plagiarism checks for some assessments" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { + "defaultMessage": "Started plagiarism check for {count, plural, =1 {# assessment} other {# assessments}}" + }, + "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { + "defaultMessage": "Search by Assessment Title" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { + "defaultMessage": "Completed" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { + "defaultMessage": "Failed" + }, "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { "defaultMessage": "Not Started" }, "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { "defaultMessage": "Running" }, - "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { - "defaultMessage": "Completed" + "course.plagiarism.PlagiarismIndex.assessments.statusStarting": { + "defaultMessage": "Starting" }, - "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { - "defaultMessage": "Failed" + "course.plagiarism.PlagiarismIndex.assessments.viewResults": { + "defaultMessage": "View Results" }, - "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { - "defaultMessage": "No checkable questions" + "course.plagiarism.PlagiarismIndex.header.plagiarism": { + "defaultMessage": "Plagiarism Check" }, - "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { - "defaultMessage": "Not enough submissions" + "course.statistics.StatisticsIndex.assessments.averageGrade": { + "defaultMessage": "Avg Grade" }, - "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { - "defaultMessage": "New Plagiarism Check ({count})" + "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { + "defaultMessage": "Avg Time" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { - "defaultMessage": "Started plagiarism check for {count, plural, =1 {# assessment} other {# assessments}}" + "course.statistics.StatisticsIndex.assessments.category": { + "defaultMessage": "Category" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { - "defaultMessage": "Failed to start plagiarism checks for some assessments" + "course.statistics.StatisticsIndex.assessments.csvFileTitle": { + "defaultMessage": "Assessments Statistics" }, - "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { - "defaultMessage": "Search by Assessment Title" + "course.statistics.StatisticsIndex.assessments.numLateStudents": { + "defaultMessage": "# Late" }, - "course.plagiarism.PlagiarismIndex.assessments.actions": { - "defaultMessage": "Actions" + "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { + "defaultMessage": "# Attempted" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { - "defaultMessage": "Run Plagiarism Check" + "course.statistics.StatisticsIndex.assessments.searchBar": { + "defaultMessage": "Search by Assessment Title, Tab, or Category" }, - "course.plagiarism.PlagiarismIndex.assessments.viewResults": { - "defaultMessage": "View Results" + "course.statistics.StatisticsIndex.assessments.startAt": { + "defaultMessage": "Starts At" }, - "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { - "defaultMessage": "New submissions detected since last plagiarism run" + "course.statistics.StatisticsIndex.assessments.stdevGrade": { + "defaultMessage": "Stdev Grade" }, - "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { - "defaultMessage": "No new submissions since last plagiarism run" + "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { + "defaultMessage": "Stdev Time" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { - "defaultMessage": "Confirm Plagiarism Check?" + "course.statistics.StatisticsIndex.assessments.subtitle": { + "defaultMessage": "To view and export individual student grades, open Gradebook." }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { - "defaultMessage": "Some of the selected assessments already have completed plagiarism checks. Running a new plagiarism check will remove the previous results." + "course.statistics.StatisticsIndex.assessments.subtitleDisabled": { + "defaultMessage": "To view and export individual student grades, enable Gradebook." }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.achievementCount": { - "defaultMessage": "No. of Achievements (Total: {courseAchievementCount})" + "course.statistics.StatisticsIndex.assessments.tab": { + "defaultMessage": "Tab" + }, + "course.statistics.StatisticsIndex.assessments.tableTitle": { + "defaultMessage": "Assessments Statistics ({numStudents} students)" + }, + "course.statistics.StatisticsIndex.assessments.title": { + "defaultMessage": "Title" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.ascending": { - "defaultMessage": "Ascending" + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.achievementCountDetails": { + "defaultMessage": "No. of Achievements (Total: {courseAchievementCount})" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.correctness": { "defaultMessage": "Correctness" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.correctnessHint": { - "defaultMessage": "Correctness is the average grade percentage of all graded assessments by a student." - }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.descending": { - "defaultMessage": "Descending" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.experiencePoints": { "defaultMessage": "Experience Points" }, @@ -6117,32 +7335,23 @@ "defaultMessage": "Tutors" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.highlight": { - "defaultMessage": "Highlight top and bottom {percent}%" + "defaultMessage": "Highlight top and bottom {percent}% based on {criteria}" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.learningRate": { "defaultMessage": "Learning Rate" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.learningRateHint": { - "defaultMessage": "A learning rate of 200% means that they can complete the course in half the time." - }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.level": { + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.levelInfo": { "defaultMessage": "Level (Max: {maxLevel})" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.levelFilter": { - "defaultMessage": "Level: {name}" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.name": { "defaultMessage": "Name" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.noData": { "defaultMessage": "No Data" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.numSubmissions": { + "course.statistics.StatisticsIndex.course.StudentPerformanceTable.numSubmissionsDetails": { "defaultMessage": "No. of Submissions (Total: {courseAssessmentCount})" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.phantom": { - "defaultMessage": "Include phantom users" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.studentType": { "defaultMessage": "Student Type" }, @@ -6152,27 +7361,15 @@ "course.statistics.StatisticsIndex.course.StudentPerformanceTable.studentType.phantom": { "defaultMessage": "Phantom" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.tableTitle": { - "defaultMessage": "Students Sorted in {direction} {column}" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.title": { "defaultMessage": "Student Performance" }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.tutorFilter": { - "defaultMessage": "Tutor: {name}" - }, - "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoPercentWatched": { - "defaultMessage": "Video % Count" - }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoPercentWatchedHeader": { "defaultMessage": "Average Video % Watched" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoSubmissionCountHeader": { "defaultMessage": "Videos Watched (Total: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.course.searchBar": { - "defaultMessage": "Search by Student Name" - }, "course.statistics.StatisticsIndex.course.StudentProgressionChart.deadlines": { "defaultMessage": "Deadlines" }, @@ -6203,89 +7400,20 @@ "course.statistics.StatisticsIndex.course.StudentProgressionChart.yAxisLabel": { "defaultMessage": "Assessment (Sorted by Deadline)" }, - "course.statistics.StatisticsIndex.course.error": { - "defaultMessage": "Something went wrong when fetching course statistics! Please refresh to try again." + "course.statistics.StatisticsIndex.course.csvFileTitle": { + "defaultMessage": "Student Performance Statistics" }, - "course.statistics.StatisticsIndex.course.performanceError": { - "defaultMessage": "Something went wrong when fetching course performance statistics! Please refresh to try again." - }, - "course.statistics.StatisticsIndex.course.progressionError": { - "defaultMessage": "Something went wrong when fetching course progression statistics! Please refresh to try again." + "course.statistics.StatisticsIndex.course.searchBar": { + "defaultMessage": "Search by Student Name" }, "course.statistics.StatisticsIndex.header.statistics": { "defaultMessage": "Statistics" }, - "course.statistics.StatisticsIndex.assessments.title": { - "defaultMessage": "Title" - }, - "course.statistics.StatisticsIndex.assessments.startAt": { - "defaultMessage": "Starts At" - }, - "course.statistics.StatisticsIndex.assessments.subtitle": { - "defaultMessage": "To view and export individual student grades, open Gradebook." - }, - "course.statistics.StatisticsIndex.assessments.subtitleDisabled": { - "defaultMessage": "To view and export individual student grades, enable Gradebook." - }, - "course.statistics.StatisticsIndex.assessments.tab": { - "defaultMessage": "Tab" - }, - "course.statistics.StatisticsIndex.assessments.category": { - "defaultMessage": "Category" - }, - "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { - "defaultMessage": "# Submitted" - }, - "course.statistics.StatisticsIndex.assessments.numAttemptedStudents": { - "defaultMessage": "# Attempted" - }, - "course.statistics.StatisticsIndex.assessments.numLateStudents": { - "defaultMessage": "# Late" - }, - "course.statistics.StatisticsIndex.assessments.averageGrade": { - "defaultMessage": "Avg Grade" - }, - "course.statistics.StatisticsIndex.assessments.stdevGrade": { - "defaultMessage": "Stdev Grade" - }, - "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { - "defaultMessage": "Avg Time" - }, - "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { - "defaultMessage": "Stdev Time" - }, - "course.statistics.StatisticsIndex.assessments.tableTitle": { - "defaultMessage": "Assessments Statistics ({numStudents} students)" - }, - "course.statistics.StatisticsIndex.assessments.csvFileTitle": { - "defaultMessage": "Assessments Statistics" - }, - "course.statistics.StatisticsIndex.assessments.searchBar": { - "defaultMessage": "Search by Assessment Title, Tab, or Category" - }, - "course.statistics.StatisticsIndex.assessments.selectedNUsers": { - "defaultMessage": "Download Score Summary for {numUsers} students?" - }, - "course.statistics.StatisticsIndex.assessments.downloadCsv": { - "defaultMessage": "Download" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummary": { - "defaultMessage": "Download Score Summary for the following Assessments?" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { - "defaultMessage": "Successfully downloaded score summary" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { - "defaultMessage": "An error occurred while downloading score summary" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { - "defaultMessage": "Your download is being processed. Please wait." - }, "course.statistics.StatisticsIndex.staff.averageMarkingTime": { "defaultMessage": "Avg Time / Assessment" }, - "course.statistics.StatisticsIndex.staff.error": { - "defaultMessage": "Something went wrong when fetching staff statistics! Please refresh to try again." + "course.statistics.StatisticsIndex.staff.csvFileTitle": { + "defaultMessage": "Staff Statistics" }, "course.statistics.StatisticsIndex.staff.name": { "defaultMessage": "Name" @@ -6296,23 +7424,20 @@ "course.statistics.StatisticsIndex.staff.numStudents": { "defaultMessage": "# Students" }, + "course.statistics.StatisticsIndex.staff.searchBar": { + "defaultMessage": "Search by Staff Name" + }, "course.statistics.StatisticsIndex.staff.stddev": { - "defaultMessage": "Standard Deviation" + "defaultMessage": "Stdev Time / Assessment" }, "course.statistics.StatisticsIndex.staff.tableTitle": { "defaultMessage": "Staff Statistics" }, - "course.statistics.StatisticsIndex.staff.csvFileTitle": { - "defaultMessage": "Staff Statistics" - }, - "course.statistics.StatisticsIndex.staff.searchBar": { - "defaultMessage": "Search by Staff Name" - }, - "course.statistics.StatisticsIndex.staffFailure": { - "defaultMessage": "Failed to fetch staff data!" + "course.statistics.StatisticsIndex.students.csvFileTitle": { + "defaultMessage": "Student Statistics" }, - "course.statistics.StatisticsIndex.students.error": { - "defaultMessage": "Something went wrong when fetching student statistics! Please refresh to try again." + "course.statistics.StatisticsIndex.students.email": { + "defaultMessage": "Email" }, "course.statistics.StatisticsIndex.students.experiencePoints": { "defaultMessage": "Experience Points" @@ -6326,14 +7451,8 @@ "course.statistics.StatisticsIndex.students.name": { "defaultMessage": "Name" }, - "course.statistics.StatisticsIndex.students.email": { - "defaultMessage": "Email" - }, - "course.statistics.StatisticsIndex.students.noStudents": { - "defaultMessage": "There is no student in this course, yet..." - }, - "course.statistics.StatisticsIndex.students.showMyStudentsOnly": { - "defaultMessage": "Show My Students Only" + "course.statistics.StatisticsIndex.students.searchBar": { + "defaultMessage": "Search by Student Name, Student Type or External ID" }, "course.statistics.StatisticsIndex.students.studentsType": { "defaultMessage": "Student Type" @@ -6341,21 +7460,12 @@ "course.statistics.StatisticsIndex.students.tableTitle": { "defaultMessage": "Student Statistics ({numStudents} students, {numPhantom} phantom)" }, - "course.statistics.StatisticsIndex.students.tutorFilter": { - "defaultMessage": "Tutor: {name}" - }, "course.statistics.StatisticsIndex.students.videoPercentWatched": { "defaultMessage": "Average % Watched" }, "course.statistics.StatisticsIndex.students.videoSubmissionCount": { "defaultMessage": "Videos Watched (Total: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.students.csvFileTitle": { - "defaultMessage": "Student Statistics" - }, - "course.statistics.StatisticsIndex.students.searchBar": { - "defaultMessage": "Search by Student Name, Student Type or External ID" - }, "course.statistics.StatisticsIndex.studentsFailure": { "defaultMessage": "Failed to fetch student data!" }, @@ -6380,12 +7490,6 @@ "course.statistics.course.studentProgressionChart.startAt": { "defaultMessage": "Starts at: {startAt}" }, - "course.statistics.failures.coursePerformance": { - "defaultMessage": "Failed to fetch course performance data!" - }, - "course.statistics.failures.courseProgression": { - "defaultMessage": "Failed to fetch course progression data!" - }, "course.statistics.tabs.course": { "defaultMessage": "Course" }, @@ -6395,6 +7499,15 @@ "course.statistics.tabs.courseProgression": { "defaultMessage": "Course Progression" }, + "course.stories.CikgoErrorPage.errorFetching": { + "defaultMessage": "Either it's supposed to be naught, or something went wrong." + }, + "course.stories.CikgoErrorPage.errorFetchingDescription": { + "defaultMessage": "Cikgo is our partner that powers this experience. They were contactable, but did not give us any resources for this request just now. Please try again later, and if this persists, contact us." + }, + "course.stories.pages.MissionControlPage": { + "defaultMessage": "Mission Control" + }, "course.survey.DeleteSectionButton.deleteSection": { "defaultMessage": "Delete Section" }, @@ -6882,7 +7995,7 @@ "defaultMessage": "Revert and delete timeline and its times" }, "course.timelines.defaultTimeline": { - "defaultMessage": "Default" + "defaultMessage": "Default Timeline" }, "course.timelines.deleteTime": { "defaultMessage": "Delete time" @@ -7040,6 +8153,30 @@ "course.userInvitation.InviteUsersRegistrationCode.registrationCodeNote": { "defaultMessage": "Users who have been invited and use this invitation code to register for the course would not have the proper status reflected in the Invitations page." }, + "course.userInvitations.ExternalIdConflictPrompt.body": { + "defaultMessage": "These users are already enrolled or have pending invitations. No new invitation emails will be sent to them. Would you like to keep their current External IDs, or replace them with the values from your file?" + }, + "course.userInvitations.ExternalIdConflictPrompt.goBack": { + "defaultMessage": "Go Back" + }, + "course.userInvitations.ExternalIdConflictPrompt.keepExisting": { + "defaultMessage": "Keep Existing" + }, + "course.userInvitations.ExternalIdConflictPrompt.pendingCourseUserUpdates": { + "defaultMessage": "Pending Course Member Updates ({count})" + }, + "course.userInvitations.ExternalIdConflictPrompt.pendingInvitationUpdates": { + "defaultMessage": "Pending Invitation Updates ({count})" + }, + "course.userInvitations.ExternalIdConflictPrompt.replace": { + "defaultMessage": "Replace" + }, + "course.userInvitations.ExternalIdConflictPrompt.title": { + "defaultMessage": "Confirm External ID Updates" + }, + "course.userInvitations.IndividualInvitations.addRowsByEmail": { + "defaultMessage": "Add Rows by Email" + }, "course.userInvitations.IndividualInvitations.appendNewRow": { "defaultMessage": "Add Row" }, @@ -7049,23 +8186,56 @@ "course.userInvitations.IndividualInvitations.invite": { "defaultMessage": "Invite All Users" }, + "course.userInvitations.IndividualInvitations.malformedEmail": { + "defaultMessage": "{n, plural, one {This email is } other {These emails are }} wrongly formatted: {emails}" + }, + "course.userInvitations.IndividualInvitations.nameEmailInput": { + "defaultMessage": "John Doe '; \"Doe, Jane\" '; ..." + }, "course.userInvitations.IndividualInvitations.namePlaceholder": { "defaultMessage": "Awesome User" }, "course.userInvitations.IndividualInvitations.removeInvitation": { "defaultMessage": "Remove Invitation" }, - "course.userInvitations.InvitationResultDialog.body": { - "defaultMessage": "{newInvitationsCount, plural, =0 {No new users were} one {# new user has been} other {# new users have been}} invited to Coursemology. {newCourseUsersCount, plural, =0 {No user with Coursemology account has been} one {# new user with existing Coursemology account has been} other {# new users with existing Coursemology accounts have been}} added to this course." + "course.userInvitations.IndividualInviteForm.failure": { + "defaultMessage": "Failed to invite users. {error}" }, - "course.userInvitations.InvitationResultDialog.close": { - "defaultMessage": "Close" + "course.userInvitations.IndividualInviteForm.failureGeneric": { + "defaultMessage": "Failed to invite users. You may reload and try again." + }, + "course.userInvitations.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" + }, + "course.userInvitations.InvitationActionButtons.deletionFailure": { + "defaultMessage": "Failed to delete user - {error}" + }, + "course.userInvitations.InvitationActionButtons.deletionFailureGeneric": { + "defaultMessage": "Failed to delete user." + }, + "course.userInvitations.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "Invitation for {name} was deleted." + }, + "course.userInvitations.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "Delete Invitation" + }, + "course.userInvitations.InvitationActionButtons.resendFailure": { + "defaultMessage": "Failed to resend invitation." + }, + "course.userInvitations.InvitationActionButtons.resendSuccess": { + "defaultMessage": "Resent email invitation to {email}!" + }, + "course.userInvitations.InvitationActionButtons.resendTooltip": { + "defaultMessage": "Resend Invitation" + }, + "course.userInvitations.InvitationResultDialog.actionableTitle": { + "defaultMessage": "Failed ({count})" }, - "course.userInvitations.InvitationResultDialog.duplicateInfo": { - "defaultMessage": "Duplicate users were found in the invitation. Only the first instance of each user will be invited." + "course.userInvitations.InvitationResultDialog.blankHeaderWarning": { + "defaultMessage": "One or more columns had no header - their data was ignored." }, - "course.userInvitations.InvitationResultDialog.duplicateUsers": { - "defaultMessage": "Duplicate Users ({count})" + "course.userInvitations.InvitationResultDialog.close": { + "defaultMessage": "Close" }, "course.userInvitations.InvitationResultDialog.existingCourseUsers": { "defaultMessage": "Existing Course Users ({count})" @@ -7073,15 +8243,18 @@ "course.userInvitations.InvitationResultDialog.existingCourseUsersInfo": { "defaultMessage": "These users are already enrolled in this course. They were not re-enrolled." }, - "course.userInvitations.InvitationResultDialog.externalIdUpdatedInfo": { - "defaultMessage": "External IDs were updated where specified." - }, "course.userInvitations.InvitationResultDialog.existingInvitations": { "defaultMessage": "Existing Invitations ({count})" }, "course.userInvitations.InvitationResultDialog.existingInvitationsInfo": { "defaultMessage": "These users already have a pending invitation. They were not re-invited." }, + "course.userInvitations.InvitationResultDialog.externalIdUpdatedInfo": { + "defaultMessage": "External IDs were updated where specified." + }, + "course.userInvitations.InvitationResultDialog.failedRowsSubtitle": { + "defaultMessage": "{count} {count, plural, one {row} other {rows}} highlighted in red could not be sent" + }, "course.userInvitations.InvitationResultDialog.header": { "defaultMessage": "Invitation Summary" }, @@ -7091,29 +8264,17 @@ "course.userInvitations.InvitationResultDialog.newInvitations": { "defaultMessage": "New Invitations ({count})" }, - "course.userInvitations.InvitationResultDialog.actionableTitle": { - "defaultMessage": "Failed ({count})" - }, - "course.userInvitations.InvitationResultDialog.failedInvitations": { - "defaultMessage": "Failed to Send ({count})" - }, - "course.userInvitations.InvitationResultDialog.failedInvitationsInfo": { - "defaultMessage": "Error occurred when sending invitation email, please try again." - }, - "course.userInvitations.InvitationResultDialog.updatedSubtitle": { - "defaultMessage": "{count} updated · shown first" - }, - "course.userInvitations.InvitationResultDialog.blankHeaderWarning": { - "defaultMessage": "One or more columns had no header - their data was ignored." - }, "course.userInvitations.InvitationResultDialog.summary": { "defaultMessage": "{newInvitations} new {newInvitations, plural, one {invitation} other {invitations}} sent, {newEnrollments} directly enrolled, {alreadyInCourse} already in course." }, "course.userInvitations.InvitationResultDialog.summaryFailed": { "defaultMessage": "{count} failed." }, - "course.userInvitations.InvitationResultDialog.failedRowsSubtitle": { - "defaultMessage": "{count} {count, plural, one {row} other {rows}} highlighted in red could not be sent" + "course.userInvitations.InvitationResultDialog.updatedSubtitle": { + "defaultMessage": "{count} updated · shown first" + }, + "course.userInvitations.InvitationResultExistingTable.previouslyLabel": { + "defaultMessage": "Previously: {value}" }, "course.userInvitations.InvitationResultFailedTable.duplicateEmailInFile": { "defaultMessage": "Duplicate email in uploaded CSV" @@ -7130,21 +8291,6 @@ "course.userInvitations.InvitationResultFailedTable.missingEmail": { "defaultMessage": "Missing email" }, - "course.userInvitations.InvitationResultSkippedTable.no": { - "defaultMessage": "No" - }, - "course.userInvitations.InvitationResultSkippedTable.previouslyLabel": { - "defaultMessage": "Previously: {value}" - }, - "course.userInvitations.InvitationResultSkippedTable.yes": { - "defaultMessage": "Yes" - }, - "course.userInvitations.InvitationResultExistingTable.previouslyLabel": { - "defaultMessage": "Previously: {value}" - }, - "course.userInvitations.InvitationsBarChart.accepted": { - "defaultMessage": "Accepted Invitations" - }, "course.userInvitations.InvitationsIndex.failure": { "defaultMessage": "Failed to fetch all invitations" }, @@ -7175,14 +8321,17 @@ "course.userInvitations.InviteUsersFileUpload.failureGeneric": { "defaultMessage": "Failed to invite users. Please ensure your data is formatted correctly." }, + "course.userInvitations.InviteUsersFileUpload.fileRequired": { + "defaultMessage": "Please select a CSV file to upload." + }, "course.userInvitations.InviteUsersFileUpload.fileUploadExample": { "defaultMessage": "Name,Email,External ID,Role,Phantom{br}John,test1@example.com,A0123456,student,y{br}Mary,test2@example.com,A0123457,teaching_assistant,n" }, "course.userInvitations.InviteUsersFileUpload.fileUploadExamplePersonalTimeline": { "defaultMessage": "Name,Email,External ID,Role,Phantom,Personal Timeline{br}John,test1@example.com,A0123456,student,y,otot{br}Mary,test2@example.com,A0123457,teaching_assistant,n,fixed" }, - "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRequired": { - "defaultMessage": "The CSV must include both a \"Name\" and \"Email\" column. All other columns are optional." + "course.userInvitations.InviteUsersFileUpload.fileUploadInfo": { + "defaultMessage": "Upload a .csv file with the following format:" }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoEmail": { "defaultMessage": "Each invitation must use a unique email address within the course. Duplicate emails will be skipped." @@ -7190,24 +8339,21 @@ "course.userInvitations.InviteUsersFileUpload.fileUploadInfoExternalId": { "defaultMessage": "If external IDs are provided, they must be unique within the course." }, - "course.userInvitations.InviteUsersFileUpload.fileUploadInfo": { - "defaultMessage": "Upload a .csv file with the following format:" - }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoPersonalTimeline": { "defaultMessage": "Personal Timelines can be [fixed, otot, stragglers, fomo], with course default: {defaultTimelineAlgorithm} if omitted." }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoPhantom": { "defaultMessage": "Phantom can be true/false with the following true values ['t', 'true', 'y', 'yes'] (case insenstitive), and defaults to false if omitted." }, + "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRequired": { + "defaultMessage": "The CSV must include both a \"Name\" and \"Email\" column. All other columns are optional." + }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRole": { "defaultMessage": "Roles can be [student, observer, teaching_assistant, manager, owner], and defaults to student if omitted. Teaching assistants can only invite users as students." }, "course.userInvitations.InviteUsersFileUpload.importInProgress": { "defaultMessage": "Importing users, please wait…" }, - "course.userInvitations.InviteUsersFileUpload.fileRequired": { - "defaultMessage": "Please select a CSV file to upload." - }, "course.userInvitations.InviteUsersFileUpload.template": { "defaultMessage": "(Template File)" }, @@ -7220,30 +8366,6 @@ "course.userInvitations.InviteUsersfileUploadForm.invite": { "defaultMessage": "Invite Users from File" }, - "course.userInvitations.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" - }, - "course.userInvitations.InvitationActionButtons.deletionFailure": { - "defaultMessage": "Failed to delete user - {error}" - }, - "course.userInvitations.InvitationActionButtons.deletionFailureGeneric": { - "defaultMessage": "Failed to delete user." - }, - "course.userInvitations.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "Invitation for {name} was deleted." - }, - "course.userInvitations.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "Delete Invitation" - }, - "course.userInvitations.InvitationActionButtons.resendFailure": { - "defaultMessage": "Failed to resend invitation." - }, - "course.userInvitations.InvitationActionButtons.resendSuccess": { - "defaultMessage": "Resent email invitation to {email}!" - }, - "course.userInvitations.InvitationActionButtons.resendTooltip": { - "defaultMessage": "Resend Invitation" - }, "course.userInvitations.RegistrationCodeButton.registrationCode": { "defaultMessage": "Registration Code" }, @@ -7262,6 +8384,9 @@ "course.userInvitations.UserInvitationsTable.accepted": { "defaultMessage": "Accepted" }, + "course.userInvitations.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "Accepted {confirmedAt}" + }, "course.userInvitations.UserInvitationsTable.failed": { "defaultMessage": "Failed" }, @@ -7271,12 +8396,12 @@ "course.userInvitations.UserInvitationsTable.pending": { "defaultMessage": "Pending" }, + "course.userInvitations.UserInvitationsTable.searchText": { + "defaultMessage": "Search by name, email or external ID" + }, "course.userInvitations.UserInvitationsTable.sentTooltip": { "defaultMessage": "Sent {sentAt}" }, - "course.userInvitations.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "Accepted {confirmedAt}" - }, "course.userNotification.AchievementGainedPopup.unlocked": { "defaultMessage": "Achievement Unlocked!" }, @@ -7295,12 +8420,6 @@ "course.users.ExperiencePointsRecords.experiencePointsHistoryHeader": { "defaultMessage": "Experience Points History: {for}" }, - "course.users.ExperiencePointsRecords.fetchUsersFailure": { - "defaultMessage": "Failed to fetch records" - }, - "course.users.ExperiencePointsTable.fetchRecordsFailure": { - "defaultMessage": "Failed to fetch records" - }, "course.users.ManageStaff.noStaff": { "defaultMessage": "No staff in course." }, @@ -7313,6 +8432,12 @@ "course.users.ManageUsersTable.ManageUsersTable.searchText": { "defaultMessage": "Search by name, email or external ID" }, + "course.users.ManageUsersTable.addIdFailure": { + "defaultMessage": "Failed to set External ID to {newId}" + }, + "course.users.ManageUsersTable.addIdSuccess": { + "defaultMessage": "External ID set to {newId}" + }, "course.users.ManageUsersTable.assignToTimeline": { "defaultMessage": "Assign to timeline" }, @@ -7343,6 +8468,12 @@ "course.users.ManageUsersTable.changeAlgorithmSuccess": { "defaultMessage": "Updated {name}'s timeline algorithm to {timeline}." }, + "course.users.ManageUsersTable.changeIdFailure": { + "defaultMessage": "Failed to change ID from {oldId} to {newId}" + }, + "course.users.ManageUsersTable.changeIdSuccess": { + "defaultMessage": "ID was changed from {oldId} to {newId}" + }, "course.users.ManageUsersTable.changeRoleFailure": { "defaultMessage": "Failed to update {name}'s role to {role}." }, @@ -7358,29 +8489,26 @@ "course.users.ManageUsersTable.defaultTimeline": { "defaultMessage": "Default" }, - "course.users.ManageUsersTable.group": { - "defaultMessage": "Group: {name}" - }, - "course.users.ManageUsersTable.phantomSuccess": { - "defaultMessage": "{name} {isPhantom, select, true {is now a phantom user} other {is now a normal user} }." + "course.users.ManageUsersTable.deleteIdFailure": { + "defaultMessage": "Failed to delete External ID" }, - "course.users.ManageUsersTable.addIdFailure": { - "defaultMessage": "Failed to set External ID to {newId}" + "course.users.ManageUsersTable.deleteIdSuccess": { + "defaultMessage": "External ID deleted" }, - "course.users.ManageUsersTable.addIdSuccess": { - "defaultMessage": "External ID set to {newId}" + "course.users.ManageUsersTable.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" }, - "course.users.ManageUsersTable.changeIdFailure": { - "defaultMessage": "Failed to change ID from {oldId} to {newId}" + "course.users.ManageUsersTable.deletionFailure": { + "defaultMessage": "Failed to delete {role} {name} ({email})." }, - "course.users.ManageUsersTable.changeIdSuccess": { - "defaultMessage": "ID was changed from {oldId} to {newId}" + "course.users.ManageUsersTable.deletionScheduled": { + "defaultMessage": "{role} {name} ({email}) has been scheduled for deletion." }, - "course.users.ManageUsersTable.deleteIdFailure": { - "defaultMessage": "Failed to delete External ID" + "course.users.ManageUsersTable.group": { + "defaultMessage": "Group: {name}" }, - "course.users.ManageUsersTable.deleteIdSuccess": { - "defaultMessage": "External ID deleted" + "course.users.ManageUsersTable.phantomSuccess": { + "defaultMessage": "{name} {isPhantom, select, true {is now a phantom user} other {is now a normal user} }." }, "course.users.ManageUsersTable.renameFailure": { "defaultMessage": "Failed to rename {oldName} to {newName}" @@ -7391,6 +8519,24 @@ "course.users.ManageUsersTable.selectedNUsers": { "defaultMessage": "Selected {n, plural, =1 {# user} other {# users}}" }, + "course.users.ManageUsersTable.suspend": { + "defaultMessage": "Suspend" + }, + "course.users.ManageUsersTable.suspendFailure": { + "defaultMessage": "Failed to suspend {name}." + }, + "course.users.ManageUsersTable.suspendSuccess": { + "defaultMessage": "{name} is now suspended. They cannot access this course until they are unsuspended." + }, + "course.users.ManageUsersTable.unsuspend": { + "defaultMessage": "Unsuspend" + }, + "course.users.ManageUsersTable.unsuspendFailure": { + "defaultMessage": "Failed to unsuspend {name}." + }, + "course.users.ManageUsersTable.unsuspendSuccess": { + "defaultMessage": "{name} is no longer suspended. They can now access the course." + }, "course.users.ManageUsersTable.updateFailure": { "defaultMessage": "Failed to update user - {error}" }, @@ -7499,36 +8645,6 @@ "course.users.UpgradeToStaff.upgradeSuccess": { "defaultMessage": "{count, plural, =0 {No users were} one {# new user has} other {# new users have}} been upgraded to {role}" }, - "course.users.ManageUsersTable.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" - }, - "course.users.ManageUsersTable.deletionFailure": { - "defaultMessage": "Failed to delete user." - }, - "course.users.ManageUsersTable.deletionScheduled": { - "defaultMessage": "{role} {name} ({email}) has been scheduled for deletion." - }, - "course.users.ManageUsersTable.deletionSuccess": { - "defaultMessage": "User was deleted." - }, - "course.users.ManageUsersTable.suspend": { - "defaultMessage": "Suspend" - }, - "course.users.ManageUsersTable.suspendFailure": { - "defaultMessage": "Failed to suspend {name}." - }, - "course.users.ManageUsersTable.suspendSuccess": { - "defaultMessage": "{name} is now suspended. They cannot access this course until they are unsuspended." - }, - "course.users.ManageUsersTable.unsuspend": { - "defaultMessage": "Unsuspend" - }, - "course.users.ManageUsersTable.unsuspendFailure": { - "defaultMessage": "Failed to unsuspend {name}." - }, - "course.users.ManageUsersTable.unsuspendSuccess": { - "defaultMessage": "{name} is no longer suspended. They can now access the course." - }, "course.users.UserManagementTabs.enrolRequestsTitle": { "defaultMessage": "Enrol Requests" }, @@ -7667,27 +8783,27 @@ "course.video.VideoShow.videoTitle": { "defaultMessage": "Video - {title}" }, + "course.video.VideoTable.actions": { + "defaultMessage": "Actions" + }, + "course.video.VideoTable.averageWatched": { + "defaultMessage": "Average % Watched" + }, "course.video.VideoTable.noVideo": { "defaultMessage": "No Video" }, - "course.video.VideoTable.title": { - "defaultMessage": "Title" + "course.video.VideoTable.published": { + "defaultMessage": "Published" }, "course.video.VideoTable.startAt": { "defaultMessage": "Start At" }, + "course.video.VideoTable.title": { + "defaultMessage": "Title" + }, "course.video.VideoTable.watchCount": { "defaultMessage": "Watch Count" }, - "course.video.VideoTable.averageWatched": { - "defaultMessage": "Average % Watched" - }, - "course.video.VideoTable.published": { - "defaultMessage": "Published" - }, - "course.video.VideoTable.actions": { - "defaultMessage": "Actions" - }, "course.video.VideosIndex.fetchVideosFailure": { "defaultMessage": "Failed to retrieve videos." }, @@ -7835,9 +8951,33 @@ "course.videoSubmissions.UserVideoSubmissionsIndex.videoSubmissionsHeader": { "defaultMessage": "Video Watch History" }, + "d6avGo": { + "defaultMessage": "Submissions" + }, + "f9aTl7": { + "defaultMessage": "Role-Playing Assessments" + }, + "jvrMfo": { + "defaultMessage": "Assistants" + }, "landing_page.create_an_account": { "defaultMessage": "Create an account" }, + "landing_page.iconEngaging": { + "defaultMessage": "Engaging" + }, + "landing_page.iconEngagingSubtitle": { + "defaultMessage": "It is built for all teachers. You do not need to have any programming knowledge to master the platform. Coursemology is easy and intuitive to use for both teachers and students." + }, + "landing_page.iconGeneral": { + "defaultMessage": "General" + }, + "landing_page.iconGeneralSubtitle": { + "defaultMessage": "It is built for all subjects. The gamification system of Coursemology does not make any assumptions on the subject. Through Coursemology, any teacher who teaches any subject can turn his course exercises into an online game." + }, + "landing_page.iconSimple": { + "defaultMessage": "Simple" + }, "landing_page.new_to_coursemology": { "defaultMessage": "New to Coursemology?" }, @@ -7874,12 +9014,12 @@ "lib.components.core.Expandable.showMore": { "defaultMessage": "Show more" }, - "lib.components.core.Note.noteHeader": { - "defaultMessage": "Note" - }, "lib.components.core.Note.errorHeader": { "defaultMessage": "Error" }, + "lib.components.core.Note.noteHeader": { + "defaultMessage": "Note" + }, "lib.components.core.banners.ServerUnreachableBanner.refreshPage": { "defaultMessage": "Refresh page" }, @@ -7973,41 +9113,107 @@ "lib.components.extensions.conditions.errorOccurredWhenCreatingCondition": { "defaultMessage": "An error occurred while creating this condition." }, - "lib.components.extensions.conditions.errorOccurredWhenDeletingCondition": { - "defaultMessage": "An error occurred while deleting this condition." + "lib.components.extensions.conditions.errorOccurredWhenDeletingCondition": { + "defaultMessage": "An error occurred while deleting this condition." + }, + "lib.components.extensions.conditions.errorOccurredWhenUpdatingCondition": { + "defaultMessage": "An error occurred while updating this condition." + }, + "lib.components.extensions.conditions.level": { + "defaultMessage": "Level" + }, + "lib.components.extensions.conditions.scoreZeroPercentNotice": { + "defaultMessage": "Note that 'scoring at least 0%' requires this assessment to be graded before this condition is fulfilled. If no minimum grade is specified, this condition only requires submission." + }, + "lib.components.extensions.conditions.scoringAtLeast": { + "defaultMessage": "scoring at least" + }, + "lib.components.extensions.conditions.specifyLevel": { + "defaultMessage": "Specify a minimum level" + }, + "lib.components.extensions.conditions.survey": { + "defaultMessage": "Survey" + }, + "lib.components.extensions.conditions.type": { + "defaultMessage": "Type" + }, + "lib.components.extensions.conditions.updateCondition": { + "defaultMessage": "Update condition" + }, + "lib.components.form.fields.DateTimePickerField.invalidDateTime": { + "defaultMessage": "Invalid Date and/or Time" + }, + "lib.components.form.fields.SingleFileInput.dropzone": { + "defaultMessage": "Drag your file here, or click to select file" + }, + "lib.components.form.fields.SingleFileInput.removeFile": { + "defaultMessage": "Remove File" + }, + "lib.components.getHelp.filter.filterAssessmentLabel": { + "defaultMessage": "Filter by Assessment" + }, + "lib.components.getHelp.filter.filterCourseLabel": { + "defaultMessage": "Filter by Course" + }, + "lib.components.getHelp.filter.filterEndDateLabel": { + "defaultMessage": "End Date" + }, + "lib.components.getHelp.filter.filterStartDateLabel": { + "defaultMessage": "Start Date" + }, + "lib.components.getHelp.filter.filterStudentLabel": { + "defaultMessage": "Filter by Student" + }, + "lib.components.getHelp.filter.lastFourteenDays": { + "defaultMessage": "Last 14 Days" + }, + "lib.components.getHelp.filter.lastSevenDays": { + "defaultMessage": "Last 7 Days" + }, + "lib.components.getHelp.filter.lastSixMonths": { + "defaultMessage": "Last 6 Months" + }, + "lib.components.getHelp.filter.lastThirtyDays": { + "defaultMessage": "Last 30 Days" + }, + "lib.components.getHelp.filter.lastTwelveMonths": { + "defaultMessage": "Last 12 Months" + }, + "lib.components.getHelp.header": { + "defaultMessage": "Recent Get Help Activity ({total, plural, one {# Conversation} other {# Conversations}})" }, - "lib.components.extensions.conditions.errorOccurredWhenUpdatingCondition": { - "defaultMessage": "An error occurred while updating this condition." + "lib.components.getHelp.table.assessmentTitle": { + "defaultMessage": "Assessment" }, - "lib.components.extensions.conditions.level": { - "defaultMessage": "Level" + "lib.components.getHelp.table.courseTitle": { + "defaultMessage": "Course" }, - "lib.components.extensions.conditions.scoreZeroPercentNotice": { - "defaultMessage": "Note that 'scoring at least 0%' requires this assessment to be graded before this condition is fulfilled. If no minimum grade is specified, this condition only requires submission." + "lib.components.getHelp.table.createdAt": { + "defaultMessage": "Last Message At" }, - "lib.components.extensions.conditions.scoringAtLeast": { - "defaultMessage": "scoring at least" + "lib.components.getHelp.table.instanceTitle": { + "defaultMessage": "Instance" }, - "lib.components.extensions.conditions.specifyLevel": { - "defaultMessage": "Specify a minimum level" + "lib.components.getHelp.table.lastMessage": { + "defaultMessage": "Last Message" }, - "lib.components.extensions.conditions.survey": { - "defaultMessage": "Survey" + "lib.components.getHelp.table.messageCount": { + "defaultMessage": "# Msgs" }, - "lib.components.extensions.conditions.type": { - "defaultMessage": "Type" + "lib.components.getHelp.table.questionNumber": { + "defaultMessage": "Question" }, - "lib.components.extensions.conditions.updateCondition": { - "defaultMessage": "Update condition" + "lib.components.getHelp.table.studentName": { + "defaultMessage": "Name" }, - "lib.components.form.fields.DateTimePickerField.invalidDateTime": { - "defaultMessage": "Invalid Date and/or Time" + "lib.components.getHelp.validation.endDateBeforeStartDate": { + "defaultMessage": "End date must be after or equal to Start date" }, - "lib.components.form.fields.SingleFileInput.dropzone": { - "defaultMessage": "Drag your file here, or click to select file" + "lib.components.getHelp.validation.exceedDateRange": { + "defaultMessage": "Date range cannot exceed 365 days" }, - "lib.components.form.fields.SingleFileInput.removeFile": { - "defaultMessage": "Remove File" + "lib.components.getHelp.validation.invalidDateSelection": { + "defaultMessage": "Invalid date" }, "lib.components.navigation.AdminPopupMenuList.adminPanel": { "defaultMessage": "System Admin Panel" @@ -8045,6 +9251,21 @@ "lib.components.navigation.CourseSwitcherPopupMenu.thisCourse": { "defaultMessage": "This course" }, + "lib.components.table.MuiColumnPickerPrompt.apply": { + "defaultMessage": "Apply to view" + }, + "lib.components.table.MuiColumnPickerPrompt.cancel": { + "defaultMessage": "Cancel" + }, + "lib.components.table.MuiColumnPickerPrompt.defaultTitle": { + "defaultMessage": "Select columns" + }, + "lib.components.table.MuiTableToolbar.directExport": { + "defaultMessage": "Export" + }, + "lib.components.table.MuiTableToolbar.exportTrigger": { + "defaultMessage": "Export…" + }, "lib.hooks.router.usePrompt.sureYouWantToLeave": { "defaultMessage": "Are you sure you want to leave this page? You will lose unsaved changes." }, @@ -8072,111 +9293,27 @@ "lib.translations.beta": { "defaultMessage": "Beta" }, - "lib.components.getHelp.header": { - "defaultMessage": "Recent Get Help Activity ({total, plural, one {# Conversation} other {# Conversations}})" - }, - "lib.components.getHelp.filter.filterCourseLabel": { - "defaultMessage": "Filter by Course" - }, - "lib.components.getHelp.filter.filterAssessmentLabel": { - "defaultMessage": "Filter by Assessment" - }, - "lib.components.getHelp.filter.filterStudentLabel": { - "defaultMessage": "Filter by Student" - }, - "lib.components.getHelp.filter.filterStartDateLabel": { - "defaultMessage": "Start Date" - }, - "lib.components.getHelp.filter.filterEndDateLabel": { - "defaultMessage": "End Date" - }, - "lib.components.getHelp.filter.lastSevenDays": { - "defaultMessage": "Last 7 Days" - }, - "lib.components.getHelp.filter.lastFourteenDays": { - "defaultMessage": "Last 14 Days" - }, - "lib.components.getHelp.filter.lastThirtyDays": { - "defaultMessage": "Last 30 Days" - }, - "lib.components.getHelp.filter.lastSixMonths": { - "defaultMessage": "Last 6 Months" - }, - "lib.components.getHelp.filter.lastTwelveMonths": { - "defaultMessage": "Last 12 Months" - }, - "lib.components.getHelp.table.studentName": { - "defaultMessage": "Name" - }, - "lib.components.getHelp.table.messageCount": { - "defaultMessage": "# Msgs" - }, - "lib.components.getHelp.table.lastMessage": { - "defaultMessage": "Last Message" - }, - "lib.components.getHelp.table.questionNumber": { - "defaultMessage": "Question" - }, - "lib.components.getHelp.table.assessmentTitle": { - "defaultMessage": "Assessment" - }, - "lib.components.getHelp.table.createdAt": { - "defaultMessage": "Last Message At" - }, - "lib.components.getHelp.table.courseTitle": { - "defaultMessage": "Course" - }, - "lib.components.getHelp.table.instanceTitle": { - "defaultMessage": "Instance" - }, - "lib.components.getHelp.validation.invalidDateSelection": { - "defaultMessage": "Invalid date" - }, - "lib.components.getHelp.validation.endDateBeforeStartDate": { - "defaultMessage": "End date must be after or equal to Start date" - }, - "lib.components.getHelp.validation.exceedDateRange": { - "defaultMessage": "Date range cannot exceed 365 days" - }, - "lib.components.table.MuiColumnPickerPrompt.apply": { - "defaultMessage": "Apply to view" - }, - "lib.components.table.MuiColumnPickerPrompt.cancel": { - "defaultMessage": "Cancel" - }, - "lib.components.table.MuiColumnPickerPrompt.defaultTitle": { - "defaultMessage": "Select columns" - }, - "lib.components.table.MuiColumnPickerPrompt.export": { - "defaultMessage": "Apply and Export" - }, - "lib.components.table.MuiTableToolbar.directExport": { - "defaultMessage": "Export" - }, - "lib.components.table.MuiTableToolbar.exportTrigger": { - "defaultMessage": "Export…" - }, "lib.translations.course.users.fetchUsersFailure": { "defaultMessage": "Failed to fetch users." }, "lib.translations.course.users.manageUsersHeader": { "defaultMessage": "Manage Users" }, - "lib.translations.course.users.roles.student": { - "defaultMessage": "Student" - }, - "lib.translations.course.users.roles.teachingAssistant": { - "defaultMessage": "Teaching Assistant" + "lib.translations.course.users.roles.manager": { + "defaultMessage": "Manager" }, "lib.translations.course.users.roles.observer": { "defaultMessage": "Observer" }, - "lib.translations.course.users.roles.manager": { - "defaultMessage": "Manager" - }, "lib.translations.course.users.roles.owner": { "defaultMessage": "Owner" }, + "lib.translations.course.users.roles.student": { + "defaultMessage": "Student" + }, + "lib.translations.course.users.roles.teachingAssistant": { + "defaultMessage": "Teaching Assistant" + }, "lib.translations.experimental": { "defaultMessage": "Experimental" }, @@ -8237,6 +9374,9 @@ "lib.translations.form.endAt": { "defaultMessage": "End At" }, + "lib.translations.form.maximum": { + "defaultMessage": "Maximum" + }, "lib.translations.form.messages.areYouSure": { "defaultMessage": "Are you sure?" }, @@ -8252,6 +9392,9 @@ "lib.translations.form.messages.unsavedChanges": { "defaultMessage": "You have unsaved changes." }, + "lib.translations.form.minimum": { + "defaultMessage": "Minimum" + }, "lib.translations.form.startAt": { "defaultMessage": "Start At" }, @@ -8288,14 +9431,17 @@ "lib.translations.form.validation.startEndDateValidationError": { "defaultMessage": "Must be after Start Date" }, - "lib.translations.instance.users.roles.normal": { - "defaultMessage": "Normal" + "lib.translations.instance.users.roles.administrator": { + "defaultMessage": "Administrator" }, "lib.translations.instance.users.roles.instructor": { "defaultMessage": "Instructor" }, - "lib.translations.instance.users.roles.administrator": { - "defaultMessage": "Administrator" + "lib.translations.instance.users.roles.normal": { + "defaultMessage": "Normal" + }, + "lib.translations.messages.featureUnavailable": { + "defaultMessage": "This feature is currently unavailable." }, "lib.translations.messages.fetchingError": { "defaultMessage": "An error occurred when loading your data. Please reload and try again." @@ -8306,9 +9452,27 @@ "lib.translations.messages.loadImageError": { "defaultMessage": "An error occurred when loading your image. Please try selecting another one." }, + "lib.translations.myStudents": { + "defaultMessage": "My Students" + }, + "lib.translations.myStudentsIncludingPhantoms": { + "defaultMessage": "My Students (Including Phantoms)" + }, "lib.translations.no": { "defaultMessage": "No" }, + "lib.translations.staff": { + "defaultMessage": "Staff" + }, + "lib.translations.staffIncludingPhantoms": { + "defaultMessage": "Staff (Including Phantoms)" + }, + "lib.translations.students": { + "defaultMessage": "Students" + }, + "lib.translations.studentsIncludingPhantoms": { + "defaultMessage": "Students (Including Phantoms)" + }, "lib.translations.summary": { "defaultMessage": "Summary" }, @@ -8345,15 +9509,15 @@ "lib.translations.table.column.createdAt": { "defaultMessage": "Created At" }, + "lib.translations.table.column.currentExternalId": { + "defaultMessage": "Current External ID" + }, "lib.translations.table.column.designation": { "defaultMessage": "Designation" }, "lib.translations.table.column.email": { "defaultMessage": "Email" }, - "lib.translations.table.column.externalId": { - "defaultMessage": "External ID" - }, "lib.translations.table.column.endAt": { "defaultMessage": "End At" }, @@ -8363,6 +9527,9 @@ "lib.translations.table.column.experiencePointsAwarded": { "defaultMessage": "EXP Awarded" }, + "lib.translations.table.column.externalId": { + "defaultMessage": "External ID" + }, "lib.translations.table.column.groups": { "defaultMessage": "Group(s)" }, @@ -8405,6 +9572,9 @@ "lib.translations.table.column.name": { "defaultMessage": "Name" }, + "lib.translations.table.column.newExternalId": { + "defaultMessage": "New External ID" + }, "lib.translations.table.column.optional": { "defaultMessage": "Optional" }, @@ -8483,33 +9653,15 @@ "lib.translations.yes": { "defaultMessage": "Yes" }, - "material.attemptLoader.errorAccessingMaterial": { - "defaultMessage": "An error occurred while accessing this material. Try again later." - }, - "system.admin.instance.instance.InstanceAdminNavigator.announcements": { - "defaultMessage": "Announcements" - }, - "system.admin.instance.instance.InstanceAdminNavigator.components": { - "defaultMessage": "Components" - }, - "system.admin.instance.instance.InstanceAdminNavigator.courses": { - "defaultMessage": "Courses" - }, - "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { - "defaultMessage": "Role Requests" - }, - "system.admin.instance.instance.InstanceAdminNavigator.users": { - "defaultMessage": "Users" - }, - "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { - "defaultMessage": "Get Help" - }, "system.admin.admin.AdminNavigator.announcements": { "defaultMessage": "System Announcements" }, "system.admin.admin.AdminNavigator.courses": { "defaultMessage": "Courses" }, + "system.admin.admin.AdminNavigator.getHelp": { + "defaultMessage": "Get Help" + }, "system.admin.admin.AdminNavigator.instances": { "defaultMessage": "Instances" }, @@ -8519,9 +9671,6 @@ "system.admin.admin.AdminNavigator.users": { "defaultMessage": "Users" }, - "system.admin.admin.AdminNavigator.getHelp": { - "defaultMessage": "Get Help" - }, "system.admin.admin.AnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "Unable to fetch announcements" }, @@ -8606,21 +9755,24 @@ "system.admin.admin.InstancesTable.updateSuccess": { "defaultMessage": "Renamed {field} from {prevValue} to {newValue}" }, + "system.admin.admin.UsersButton.associatedCourses": { + "defaultMessage": "{courseName} ({instanceName})" + }, "system.admin.admin.UsersButton.deleteTooltip": { "defaultMessage": "Delete User" }, "system.admin.admin.UsersButton.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete {role} {name} ({email})?" + "defaultMessage": "Are you sure you wish to proceed?" }, "system.admin.admin.UsersButton.deletionConfirmTitle": { "defaultMessage": "Deleting {role} User {name} ({email})" }, - "system.admin.admin.UsersButton.deletionPromptContent": { - "defaultMessage": "Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:" - }, "system.admin.admin.UsersButton.deletionFailure": { "defaultMessage": "Failed to delete user - {error}" }, + "system.admin.admin.UsersButton.deletionPromptContent": { + "defaultMessage": "Deleting this user will PERMANENTLY delete associated data in the following {count, plural, one {course} other {courses}}:" + }, "system.admin.admin.UsersButton.deletionSuccess": { "defaultMessage": "User was deleted." }, @@ -8654,17 +9806,35 @@ "system.admin.instance.instance.IndividualInvitation.emailPlaceholder": { "defaultMessage": "user@example.com" }, - "system.admin.instance.instance.IndividualInvitation.namePlaceholder": { - "defaultMessage": "Name" + "system.admin.instance.instance.IndividualInvitation.namePlaceholder": { + "defaultMessage": "Name" + }, + "system.admin.instance.instance.IndividualInvitation.removeInvitation": { + "defaultMessage": "Remove Invitation" + }, + "system.admin.instance.instance.IndividualInvitations.appendNewRow": { + "defaultMessage": "Add Row" + }, + "system.admin.instance.instance.IndividualInvitations.invite": { + "defaultMessage": "Invite All Users" + }, + "system.admin.instance.instance.InstanceAdminNavigator.announcements": { + "defaultMessage": "Announcements" + }, + "system.admin.instance.instance.InstanceAdminNavigator.components": { + "defaultMessage": "Components" + }, + "system.admin.instance.instance.InstanceAdminNavigator.courses": { + "defaultMessage": "Courses" }, - "system.admin.instance.instance.IndividualInvitation.removeInvitation": { - "defaultMessage": "Remove Invitation" + "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { + "defaultMessage": "Get Help" }, - "system.admin.instance.instance.IndividualInvitations.appendNewRow": { - "defaultMessage": "Add Row" + "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { + "defaultMessage": "Role Requests" }, - "system.admin.instance.instance.IndividualInvitations.invite": { - "defaultMessage": "Invite All Users" + "system.admin.instance.instance.InstanceAdminNavigator.users": { + "defaultMessage": "Users" }, "system.admin.instance.instance.InstanceAnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "Unable to fetch announcements" @@ -8759,21 +9929,12 @@ "system.admin.instance.instance.InstanceUsersIndex.totalUsers": { "defaultMessage": "Total Users: {allCount} ({adminCount} Administrators, {instructorCount} Instructors, {normalCount} Normal)" }, - "system.admin.instance.instance.InstanceUsersInvitations.accepted": { - "defaultMessage": "Accepted Invitations" - }, - "system.admin.instance.instance.InstanceUsersInvitations.failed": { - "defaultMessage": "Failed Invitations" - }, "system.admin.instance.instance.InstanceUsersInvitations.fetch.failure": { "defaultMessage": "Failed to fetch invitations." }, "system.admin.instance.instance.InstanceUsersInvitations.header": { "defaultMessage": "Invitations" }, - "system.admin.instance.instance.InstanceUsersInvitations.pending": { - "defaultMessage": "Pending Invitations" - }, "system.admin.instance.instance.InstanceUsersInvitations.title": { "defaultMessage": "Users" }, @@ -8786,6 +9947,27 @@ "system.admin.instance.instance.InstanceUsersTabs.usersTab": { "defaultMessage": "Users" }, + "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { + "defaultMessage": "Failed to delete user - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "Invitation for {name} was deleted." + }, + "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "Delete Invitation" + }, + "system.admin.instance.instance.InvitationActionButtons.resendFailure": { + "defaultMessage": "Failed to resend invitation - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { + "defaultMessage": "Resent email invitation to {email}!" + }, + "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { + "defaultMessage": "Resend Invitation" + }, "system.admin.instance.instance.InvitationResultDialog.close": { "defaultMessage": "Close" }, @@ -8816,27 +9998,6 @@ "system.admin.instance.instance.InvitationResultDialog.newInvitations": { "defaultMessage": "New Invitations ({count})" }, - "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "Are you sure you wish to delete invitation to {name} ({email})?" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { - "defaultMessage": "Failed to delete user - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "Invitation for {name} was deleted." - }, - "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "Delete Invitation" - }, - "system.admin.instance.instance.InvitationActionButtons.resendFailure": { - "defaultMessage": "Failed to resend invitation - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { - "defaultMessage": "Resent email invitation to {email}!" - }, - "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { - "defaultMessage": "Resend Invitation" - }, "system.admin.instance.instance.PendingRoleRequestsButton.approveFailure": { "defaultMessage": "Failed to approve role request - {error}" }, @@ -8882,6 +10043,9 @@ "system.admin.instance.instance.UserInvitationsTable.accepted": { "defaultMessage": "Accepted" }, + "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "Accepted {confirmedAt}" + }, "system.admin.instance.instance.UserInvitationsTable.failed": { "defaultMessage": "Failed" }, @@ -8894,9 +10058,6 @@ "system.admin.instance.instance.UserInvitationsTable.sentTooltip": { "defaultMessage": "Sent {sentAt}" }, - "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "Accepted {confirmedAt}" - }, "system.admin.instance.instance.UsersButton.deleteTooltip": { "defaultMessage": "Remove User" }, @@ -8906,12 +10067,12 @@ "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { "defaultMessage": "Removing {role} User {name} ({email})" }, - "system.admin.instance.instance.UsersButton.deletionPromptContent": { - "defaultMessage": "Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:" - }, "system.admin.instance.instance.UsersButton.deletionFailure": { "defaultMessage": "Failed to remove user - {error}" }, + "system.admin.instance.instance.UsersButton.deletionPromptContent": { + "defaultMessage": "Removing this user may cause errors in the following {count, plural, one {course} other {courses}}:" + }, "system.admin.instance.instance.UsersButton.deletionSuccess": { "defaultMessage": "User was removed from this instance." }, @@ -9242,190 +10403,7 @@ "users.troubleSigningIn": { "defaultMessage": "Trouble signing in?" }, - "course.userInvitations.ExternalIdConflictPrompt.title": { - "defaultMessage": "Confirm External ID Updates" - }, - "course.userInvitations.ExternalIdConflictPrompt.body": { - "defaultMessage": "These users are already enrolled or have pending invitations. No new invitation emails will be sent to them. Would you like to keep their current External IDs, or replace them with the values from your file?" - }, - "course.userInvitations.ExternalIdConflictPrompt.pendingInvitationUpdates": { - "defaultMessage": "Pending Invitation Updates ({count})" - }, - "course.userInvitations.ExternalIdConflictPrompt.pendingCourseUserUpdates": { - "defaultMessage": "Pending Course Member Updates ({count})" - }, - "course.userInvitations.ExternalIdConflictPrompt.goBack": { - "defaultMessage": "Go Back" - }, - "course.userInvitations.ExternalIdConflictPrompt.keepExisting": { - "defaultMessage": "Keep Existing" - }, - "course.userInvitations.ExternalIdConflictPrompt.replace": { - "defaultMessage": "Replace" - }, - "lib.translations.table.column.currentExternalId": { - "defaultMessage": "Current External ID" - }, - "lib.translations.table.column.newExternalId": { - "defaultMessage": "New External ID" - }, - "course.admin.GradebookSettings.gradebookSettings": { - "defaultMessage": "Gradebook settings" - }, - "course.admin.GradebookSettings.weightedViewEnabled": { - "defaultMessage": "Enable weighted grade view" - }, - "course.admin.GradebookSettings.weightedViewEnabledHint": { - "defaultMessage": "Enables a \"Weighted total\" view in the gradebook where staff can configure per-tab weights and see a weighted Total column." - }, - "course.gradebook.ConfigureWeightsPrompt.allExcluded": { - "defaultMessage": "All assessments in \"{tab}\" are excluded — it contributes nothing to the total." - }, - "course.gradebook.ConfigureWeightsPrompt.allExcludedCount": { - "defaultMessage": "All {n} excluded" - }, - "course.gradebook.ConfigureWeightsPrompt.customMode": { - "defaultMessage": "Custom" - }, - "course.gradebook.ConfigureWeightsPrompt.customSum": { - "defaultMessage": "Assessment weights: {sum} / {total}" - }, - "course.gradebook.ConfigureWeightsPrompt.defaultsHint": { - "defaultMessage": "No weights set yet — these are suggested defaults with every tab counting equally. Save to confirm, or adjust below." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionDrop": { - "defaultMessage": "In Equal mode, optionally drop each student's N lowest-scoring assessments before averaging." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionExclusion": { - "defaultMessage": "Expand a tab to include or exclude individual assessments from grading." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionIntro": { - "defaultMessage": "Control how tabs and assessments count toward each student's total grade." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionModes": { - "defaultMessage": "Choose Equal (all assessments share the tab's weight) or Custom (set each assessment's share)." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionWeights": { - "defaultMessage": "Set each tab's weight — how much it contributes to the total (weights should sum to 100)." - }, - "course.gradebook.ConfigureWeightsPrompt.equalMode": { - "defaultMessage": "Equal" - }, - "course.gradebook.ConfigureWeightsPrompt.excluded": { - "defaultMessage": "Excluded" - }, - "course.gradebook.ConfigureWeightsPrompt.excludedCount": { - "defaultMessage": "{n} excluded" - }, - "course.gradebook.ConfigureWeightsPrompt.includeAssessment": { - "defaultMessage": "Include {assessment} in grade" - }, - "course.gradebook.ConfigureWeightsPrompt.modeAria": { - "defaultMessage": "{tab} weight mode" - }, - "course.gradebook.ConfigureWeightsPrompt.ofGrade": { - "defaultMessage": "{pct}% of grade" - }, - "course.gradebook.ConfigureWeightsPrompt.promptTitle": { - "defaultMessage": "Configure contributions" - }, - "course.gradebook.ConfigureWeightsPrompt.saveError": { - "defaultMessage": "Failed to save weights. Please try again." - }, - "course.gradebook.ConfigureWeightsPrompt.total": { - "defaultMessage": "Total: {sum}%" - }, - "course.gradebook.ConfigureWeightsPrompt.unbalanced": { - "defaultMessage": "Assessment weights for \"{tab}\" must sum to its tab total before saving." - }, - "course.gradebook.ConfigureWeightsPrompt.valueTooHigh": { - "defaultMessage": "Value must be at most 100" - }, - "course.gradebook.ConfigureWeightsPrompt.valueTooLow": { - "defaultMessage": "Value must be at least 0" - }, - "course.gradebook.ConfigureWeightsPrompt.weightsDoNotSum": { - "defaultMessage": "Weights do not sum to 100. Saving is allowed; Total may be inaccurate." - }, - "course.gradebook.GradebookIndex.allAssessments": { - "defaultMessage": "All assessments" - }, - "course.gradebook.GradebookIndex.byWeight": { - "defaultMessage": "Weighted total" - }, - "course.gradebook.GradebookWeightedTable.collapseRow": { - "defaultMessage": "Collapse {name}" - }, - "course.gradebook.GradebookWeightedTable.configureWeights": { - "defaultMessage": "Configure Weights" - }, - "course.gradebook.GradebookWeightedTable.defaultWeights": { - "defaultMessage": "Showing default weights — every tab counts equally. Click \"Configure Weights\" to set your own." - }, - "course.gradebook.GradebookWeightedTable.defaultWeightsNoAccess": { - "defaultMessage": "Showing default weights — every tab counts equally until weights are configured." - }, - "course.gradebook.GradebookWeightedTable.displayPercent": { - "defaultMessage": "Percentage" - }, - "course.gradebook.GradebookWeightedTable.displayPercentTooltip": { - "defaultMessage": "What fraction of each tab the student earned. 100% on a tab worth 20% = the student earned all 20 grade points from that tab." - }, - "course.gradebook.GradebookWeightedTable.displayPoints": { - "defaultMessage": "Points" - }, - "course.gradebook.GradebookWeightedTable.displayPointsTooltip": { - "defaultMessage": "How many grade points each tab contributes. Columns add up to the projected total." - }, - "course.gradebook.GradebookWeightedTable.downloadCsv": { - "defaultMessage": "Download as CSV" - }, - "course.gradebook.GradebookWeightedTable.email": { - "defaultMessage": "Email" - }, - "course.gradebook.GradebookWeightedTable.excluded": { - "defaultMessage": "Excluded" - }, - "course.gradebook.GradebookWeightedTable.expandRow": { - "defaultMessage": "Expand {name}" - }, - "course.gradebook.GradebookWeightedTable.name": { - "defaultMessage": "Name" - }, - "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { - "defaultMessage": "No weights configured — all tab weights are 0. Click \"Configure Weights\" to assign weights." - }, - "course.gradebook.GradebookWeightedTable.noWeightsNoAccess": { - "defaultMessage": "No tab weights have been configured yet." - }, - "course.gradebook.GradebookWeightedTable.outOfWeight": { - "defaultMessage": "/{weight}" - }, - "course.gradebook.GradebookWeightedTable.percentOfGrade": { - "defaultMessage": "{weight}% of grade" - }, - "course.gradebook.GradebookWeightedTable.percentTotalExact": { - "defaultMessage": "100% total" - }, - "course.gradebook.GradebookWeightedTable.percentTotalWarning": { - "defaultMessage": "{weight}% total" - }, - "course.gradebook.GradebookWeightedTable.total": { - "defaultMessage": "Total" - }, - "course.gradebook.GradebookWeightedTable.searchStudents": { - "defaultMessage": "Search students" - }, - "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { - "defaultMessage": "Weights do not sum to 100. Total may be inaccurate." - }, - "course.gradebook.TotalHint.policy": { - "defaultMessage": "Totals count ungraded assessments as 0." - }, - "course.gradebook.WeightedViewHint.hint": { - "defaultMessage": "Want a weighted total grade? You can set how much each tab counts toward each student’s overall grade and view the weighted total here. Turn it on in {link}." - }, - "course.gradebook.WeightedViewHint.settingsLink": { - "defaultMessage": "Gradebook settings" + "wEQDC6": { + "defaultMessage": "Edit" } -} \ No newline at end of file +} diff --git a/client/locales/ko.json b/client/locales/ko.json index 9f9c0c593c..f42bac09c4 100644 --- a/client/locales/ko.json +++ b/client/locales/ko.json @@ -29,6 +29,9 @@ "app.DashboardPage.yourCourses": { "defaultMessage": "내 강좌" }, + "app.ErrorPage.courseSuspended": { + "defaultMessage": "이 과정은 현재 정지되어 있습니다." + }, "app.ErrorPage.error": { "defaultMessage": "쾅, 운석이 떨어졌습니다." }, @@ -59,14 +62,11 @@ "app.ErrorPage.notFoundSubtitle": { "defaultMessage": "주소를 올바르게 입력했는지 확인하고, 나중에 다시 시도하거나, 홈으로 돌아가기를 클릭하세요." }, - "app.ErrorPage.userSuspended": { - "defaultMessage": "이 과정에 대한 접근이 정지되었습니다." - }, "app.ErrorPage.suspendedSubtitle": { "defaultMessage": "강사 또는 과정 담당자에게 문의하세요." }, - "app.ErrorPage.courseSuspended": { - "defaultMessage": "이 과정은 현재 정지되어 있습니다." + "app.ErrorPage.userSuspended": { + "defaultMessage": "이 과정에 대한 접근이 정지되었습니다." }, "app.Footer.contactUs": { "defaultMessage": "문의하기" @@ -80,12 +80,12 @@ "app.Footer.instructorsGuide": { "defaultMessage": "교사 안내서" }, - "app.Footer.reportIssue": { - "defaultMessage": "문제 신고" - }, "app.Footer.privacyPolicy": { "defaultMessage": "개인정보 보호정책" }, + "app.Footer.reportIssue": { + "defaultMessage": "문제 신고" + }, "app.Footer.termsOfService": { "defaultMessage": "서비스 약관" }, @@ -263,23 +263,23 @@ "course.achievement.AchievementAward.AchievementAwardManager.saveChanges": { "defaultMessage": "변경 사항 저장" }, + "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { + "defaultMessage": "부여된 학생" + }, "course.achievement.AchievementAward.AchievementAwardSummary.name": { "defaultMessage": "이름" }, - "course.achievement.AchievementAward.AchievementAwardSummary.userType": { - "defaultMessage": "사용자 유형" + "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { + "defaultMessage": "일반 학생" }, - "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { - "defaultMessage": "부여된 학생" + "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { + "defaultMessage": "팬텀 학생" }, "course.achievement.AchievementAward.AchievementAwardSummary.revokedStudents": { "defaultMessage": "철회된 학생" }, - "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { - "defaultMessage": "팬텀 학생" - }, - "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { - "defaultMessage": "일반 학생" + "course.achievement.AchievementAward.AchievementAwardSummary.userType": { + "defaultMessage": "사용자 유형" }, "course.achievement.AchievementAward.awardAchievement": { "defaultMessage": "성과 부여" @@ -353,26 +353,26 @@ "course.achievement.AchievementShow.studentsWithAchievement": { "defaultMessage": "이 성과를 가진 학생들" }, - "course.achievement.AchievementTable.noAchievement": { - "defaultMessage": "성과 없음" + "course.achievement.AchievementTable.actions": { + "defaultMessage": "작업" }, "course.achievement.AchievementTable.badge": { "defaultMessage": "배지" }, - "course.achievement.AchievementTable.title": { - "defaultMessage": "제목" - }, "course.achievement.AchievementTable.description": { "defaultMessage": "설명" }, - "course.achievement.AchievementTable.requirements": { - "defaultMessage": "요구 사항" + "course.achievement.AchievementTable.noAchievement": { + "defaultMessage": "성과 없음" }, "course.achievement.AchievementTable.published": { "defaultMessage": "게시됨" }, - "course.achievement.AchievementTable.actions": { - "defaultMessage": "작업" + "course.achievement.AchievementTable.requirements": { + "defaultMessage": "요구 사항" + }, + "course.achievement.AchievementTable.title": { + "defaultMessage": "제목" }, "course.achievement.AchievementsIndex.achievements": { "defaultMessage": "성과" @@ -530,47 +530,29 @@ "course.admin.CodaveriSettings.codaveriEngineDescription": { "defaultMessage": "프로그래밍 코드 피드백을 생성하는 데 사용되는 코다베리 엔진 유형" }, + "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { + "defaultMessage": "코다베리 평가자" + }, + "course.admin.CodaveriSettings.codaveriModel": { + "defaultMessage": "모델" + }, + "course.admin.CodaveriSettings.codaveriModelDescription": { + "defaultMessage": "Codaveri가 프로그래밍 문제에 대한 학생 도움 대화를 생성하는 데 사용하는 AI 모델입니다." + }, "course.admin.CodaveriSettings.codaveriOverrideSystemPrompt": { "defaultMessage": "사용자 정의 시스템 프롬프트 사용" }, "course.admin.CodaveriSettings.codaveriOverrideSystemPromptDescription": { "defaultMessage": "학생을 도울 때, 이 지침은 문제 자체에 설정한 지침에 추가로 따르게 됩니다. 문제별 세부 사항을 참조하려면 프롬프트 내에서 다음 변수를 사용할 수 있으며, 아래와 같이 대괄호로 작성합니다:" }, - "course.admin.CodaveriSettings.codaveriSystemPrompt": { - "defaultMessage": "시스템 프롬프트" - }, - "course.admin.CodaveriSettings.codaveriUseDefaultSystemPrompt": { - "defaultMessage": "기본 시스템 프롬프트 사용" - }, - "course.admin.CodaveriSettings.evaluatorUpdateSuccess": { - "defaultMessage": "{question}이(가) 이제 {evaluator} 평가자를 사용합니다" - }, - "course.admin.CodaveriSettings.expandAll": { - "defaultMessage": "모든 질문 펼치기" - }, - "course.admin.CodaveriSettings.programmingQuestionSettings": { - "defaultMessage": "프로그래밍 질문 설정" - }, - "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { - "defaultMessage": "다양한 평가의 프로그래밍 질문에 대해 코다베리를 평가자로 활성화/비활성화합니다." - }, - "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { - "defaultMessage": "모든 질문을 {evaluator} 평가자를 사용하도록 성공적으로 업데이트했습니다" - }, - "course.admin.CodaveriSettings.getHelpUsageLimit": { - "defaultMessage": "학생당 도움 요청 메시지 제한" - }, - "course.admin.CodaveriSettings.getHelpUsageLimitDescription": { - "defaultMessage": "활성화되면 학생은 질문당 제한된 수의 메시지만 보낼 수 있습니다. 학생은 이 제한과 남은 메시지 수를 볼 수 있습니다." - }, - "course.admin.CodaveriSettings.maxGetHelpUserMessages": { - "defaultMessage": "질문당 최대 메시지 수" + "course.admin.CodaveriSettings.codaveriSettings": { + "defaultMessage": "코다베리 설정" }, - "course.admin.CodaveriSettings.codaveriModel": { - "defaultMessage": "모델" + "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { + "defaultMessage": "현재 실험적인 기능입니다. 코다베리는 학생의 코드에 대한 코드 평가 및 자동 코드 피드백 서비스를 제공합니다." }, - "course.admin.CodaveriSettings.codaveriModelDescription": { - "defaultMessage": "Codaveri가 프로그래밍 문제에 대한 학생 도움 대화를 생성하는 데 사용하는 AI 모델입니다." + "course.admin.CodaveriSettings.codaveriSystemPrompt": { + "defaultMessage": "시스템 프롬프트" }, "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { "defaultMessage": "여기에서 지침을 제공하여 Codaveri 모델의 동작을 사용자 지정할 수 있습니다.{br} 학생을 도울 때, 이 지침은 문제 자체에 설정한 지침에 추가로 따르게 됩니다.{br} 문제별 세부 사항을 참조하려면, 아래와 같이 대괄호로 작성된 다음 변수를 프롬프트에서 사용할 수 있습니다:" @@ -581,14 +563,8 @@ "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { "defaultMessage": "{studentFilePathsVar} : 학생이 작업 중인 파일 경로의 쉼표로 구분된 목록" }, - "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { - "defaultMessage": "코다베리 평가자" - }, - "course.admin.CodaveriSettings.codaveriSettings": { - "defaultMessage": "코다베리 설정" - }, - "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { - "defaultMessage": "현재 실험적인 기능입니다. 코다베리는 학생의 코드에 대한 코드 평가 및 자동 코드 피드백 서비스를 제공합니다." + "course.admin.CodaveriSettings.codaveriUseDefaultSystemPrompt": { + "defaultMessage": "기본 시스템 프롬프트 사용" }, "course.admin.CodaveriSettings.enableDisableButton": { "defaultMessage": "{enabled, select, true {활성화} other {비활성화}}" @@ -611,6 +587,12 @@ "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { "defaultMessage": "실시간 피드백 설정을 업데이트하는 동안 오류가 발생했습니다." }, + "course.admin.CodaveriSettings.evaluatorUpdateSuccess": { + "defaultMessage": "{question}이(가) 이제 {evaluator} 평가자를 사용합니다" + }, + "course.admin.CodaveriSettings.expandAll": { + "defaultMessage": "모든 질문 펼치기" + }, "course.admin.CodaveriSettings.feedbackWorkflow": { "defaultMessage": "자동 게시 제출 댓글" }, @@ -626,12 +608,30 @@ "course.admin.CodaveriSettings.feedbackWorkflowPublish": { "defaultMessage": "학생에게 직접 피드백 게시" }, + "course.admin.CodaveriSettings.getHelpUsageLimit": { + "defaultMessage": "학생당 도움 요청 메시지 제한" + }, + "course.admin.CodaveriSettings.getHelpUsageLimitDescription": { + "defaultMessage": "활성화되면 학생은 질문당 제한된 수의 메시지만 보낼 수 있습니다. 학생은 이 제한과 남은 메시지 수를 볼 수 있습니다." + }, "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { "defaultMessage": "{question}에 대한 실시간 피드백이 {liveFeedbackEnabled, select, true {활성화} other {비활성화}}되었습니다" }, "course.admin.CodaveriSettings.liveFeedbackSettings": { "defaultMessage": "실시간 피드백" }, + "course.admin.CodaveriSettings.maxGetHelpUserMessages": { + "defaultMessage": "질문당 최대 메시지 수" + }, + "course.admin.CodaveriSettings.programmingQuestionSettings": { + "defaultMessage": "프로그래밍 질문 설정" + }, + "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { + "defaultMessage": "다양한 평가의 프로그래밍 질문에 대해 코다베리를 평가자로 활성화/비활성화합니다." + }, + "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { + "defaultMessage": "모든 질문을 {evaluator} 평가자를 사용하도록 성공적으로 업데이트했습니다" + }, "course.admin.CodaveriSettings.successfulUpdateAllLiveFeedbackEnabled": { "defaultMessage": "모든 질문에 대해 {liveFeedbackEnabled, select, true {활성화} other {비활성화}}된 실시간 피드백이 성공적으로 {liveFeedbackEnabled, select, true {활성화} other {비활성화}}되었습니다" }, @@ -677,6 +677,12 @@ "course.admin.CourseSettings.courseSettings": { "defaultMessage": "강좌 설정" }, + "course.admin.CourseSettings.courseSuspensionMessage": { + "defaultMessage": "과정 정지 메시지" + }, + "course.admin.CourseSettings.courseSuspensionMessageDescription": { + "defaultMessage": "이 메시지는 과정이 정지된 동안 사용자에게 표시됩니다. 기본 메시지를 표시하려면 비워 두세요." + }, "course.admin.CourseSettings.daysInAdvance": { "defaultMessage": "몇 일 전" }, @@ -764,23 +770,14 @@ "course.admin.CourseSettings.stragglersDescription": { "defaultMessage": "뒤처진 학생을 위해 다음 폐쇄 참조 시간이 연기됩니다." }, - "course.admin.CourseSettings.suspension": { - "defaultMessage": "접근 정지" - }, "course.admin.CourseSettings.suspendCourse": { "defaultMessage": "과정 정지" }, "course.admin.CourseSettings.suspendCourseDescription": { "defaultMessage": "정지된 과정은 모든 학생이 접근할 수 없습니다. 강사는 과정에 계속 접근할 수 있으며 모든 학생 데이터는 보존됩니다." }, - "course.admin.CourseSettings.unsuspendCourse": { - "defaultMessage": "과정 정지 해제" - }, - "course.admin.CourseSettings.courseSuspensionMessage": { - "defaultMessage": "과정 정지 메시지" - }, - "course.admin.CourseSettings.courseSuspensionMessageDescription": { - "defaultMessage": "이 메시지는 과정이 정지된 동안 사용자에게 표시됩니다. 기본 메시지를 표시하려면 비워 두세요." + "course.admin.CourseSettings.suspendCourseFailure": { + "defaultMessage": "과정을 정지하는 중 오류가 발생했습니다." }, "course.admin.CourseSettings.suspendCoursePromptText": { "defaultMessage": "이 과정을 정지하시겠습니까? 정지가 해제될 때까지 모든 학생이 접근할 수 없게 됩니다." @@ -788,20 +785,8 @@ "course.admin.CourseSettings.suspendCourseSuccess": { "defaultMessage": "이 과정이 정지되었습니다." }, - "course.admin.CourseSettings.suspendCourseFailure": { - "defaultMessage": "과정을 정지하는 중 오류가 발생했습니다." - }, - "course.admin.CourseSettings.unsuspendCourseSuccess": { - "defaultMessage": "이 과정의 정지가 해제되었습니다." - }, - "course.admin.CourseSettings.unsuspendCourseFailure": { - "defaultMessage": "과정 정지를 해제하는 중 오류가 발생했습니다." - }, - "course.admin.CourseSettings.userSuspensionMessage": { - "defaultMessage": "사용자 정지 메시지" - }, - "course.admin.CourseSettings.userSuspensionMessageDescription": { - "defaultMessage": "이 메시지는 이 과정에 대한 접근이 정지된 개별 사용자에게 표시됩니다. 기본 메시지를 표시하려면 비워 두세요." + "course.admin.CourseSettings.suspension": { + "defaultMessage": "접근 정지" }, "course.admin.CourseSettings.timeSettings": { "defaultMessage": "시간 설정" @@ -812,12 +797,27 @@ "course.admin.CourseSettings.titleRequired": { "defaultMessage": "강좌 이름이 필요합니다." }, + "course.admin.CourseSettings.unsuspendCourse": { + "defaultMessage": "과정 정지 해제" + }, + "course.admin.CourseSettings.unsuspendCourseFailure": { + "defaultMessage": "과정 정지를 해제하는 중 오류가 발생했습니다." + }, + "course.admin.CourseSettings.unsuspendCourseSuccess": { + "defaultMessage": "이 과정의 정지가 해제되었습니다." + }, "course.admin.CourseSettings.uploadANewImage": { "defaultMessage": "새 이미지 선택" }, "course.admin.CourseSettings.uploadingLogo": { "defaultMessage": "새 로고를 업로드하는 중..." }, + "course.admin.CourseSettings.userSuspensionMessage": { + "defaultMessage": "사용자 정지 메시지" + }, + "course.admin.CourseSettings.userSuspensionMessageDescription": { + "defaultMessage": "이 메시지는 이 과정에 대한 접근이 정지된 개별 사용자에게 표시됩니다. 기본 메시지를 표시하려면 비워 두세요." + }, "course.admin.CourseSettingst.confirmDeletePlaceholder": { "defaultMessage": "되돌릴 기회는 이것이 마지막입니다!" }, @@ -872,6 +872,15 @@ "course.admin.ForumsSettings.markPostAsAnswerSetting": { "defaultMessage": "게시물을 답변으로 표시할 사용자" }, + "course.admin.GradebookSettings.gradebookSettings": { + "defaultMessage": "성적부 설정" + }, + "course.admin.GradebookSettings.weightedViewEnabled": { + "defaultMessage": "가중 성적 보기 사용" + }, + "course.admin.GradebookSettings.weightedViewEnabledHint": { + "defaultMessage": "성적부에 \"가중 총점\" 보기를 추가하여 교직원이 탭별 가중치를 설정하고 가중 총점 열을 확인할 수 있습니다." + }, "course.admin.LeaderboardSettings.displayUserCount": { "defaultMessage": "사용자 수 표시" }, @@ -941,6 +950,9 @@ "course.admin.MaterialSettings.materialsSettings": { "defaultMessage": "자료 설정" }, + "course.admin.NotificationSettings.component": { + "defaultMessage": "구성 요소" + }, "course.admin.NotificationSettings.description": { "defaultMessage": "설명" }, @@ -1394,12 +1406,12 @@ "course.assessment.AssessmentForm.showMcqMrqSolution": { "defaultMessage": "MCQ/MRQ 솔루션 표시" }, - "course.assessment.AssessmentForm.showRubricToStudents": { - "defaultMessage": "학생에게 루브릭 세부사항 표시" - }, "course.assessment.AssessmentForm.showPrivate": { "defaultMessage": "개인 테스트 케이스 표시" }, + "course.assessment.AssessmentForm.showRubricToStudents": { + "defaultMessage": "학생에게 루브릭 세부사항 표시" + }, "course.assessment.AssessmentForm.singlePage": { "defaultMessage": "단일 페이지" }, @@ -1505,9 +1517,24 @@ "course.assessment.edit.update": { "defaultMessage": "저장" }, + "course.assessment.generation.allFieldsLocked": { + "defaultMessage": "모든 필드가 잠겨 있어 생성을 진행할 수 없습니다." + }, "course.assessment.generation.confirmDeleteConversation": { "defaultMessage": "\"{title}\" 및 모든 기록 항목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다!" }, + "course.assessment.generation.createMode": { + "defaultMessage": "새로 만들기" + }, + "course.assessment.generation.createModeTooltip": { + "defaultMessage": "처음부터 새로운 질문을 생성합니다" + }, + "course.assessment.generation.enhanceMode": { + "defaultMessage": "강화" + }, + "course.assessment.generation.enhanceModeTooltip": { + "defaultMessage": "현재 질문을 기반으로 확장합니다" + }, "course.assessment.generation.exportAction": { "defaultMessage": "내보내기" }, @@ -1517,123 +1544,75 @@ "course.assessment.generation.exportError": { "defaultMessage": "이 문제를 내보내는 중 오류가 발생했습니다: {error}" }, + "course.assessment.generation.generateError": { + "defaultMessage": "{title} 문항 생성 중 오류가 발생했습니다." + }, + "course.assessment.generation.generateMcqPage": { + "defaultMessage": "객관식 문항 생성" + }, + "course.assessment.generation.generateMrqPage": { + "defaultMessage": "다중 선택 문항 생성" + }, + "course.assessment.generation.generateMultipleSuccess": { + "defaultMessage": "{count}개의 문항이 성공적으로 생성되었습니다!" + }, + "course.assessment.generation.generateQuestion": { + "defaultMessage": "생성" + }, + "course.assessment.generation.generateSuccess": { + "defaultMessage": "{title} 생성에 성공했습니다." + }, + "course.assessment.generation.loadingSourceError": { + "defaultMessage": "원본 문항 데이터를 불러올 수 없습니다." + }, "course.assessment.generation.lockTooltip": { "defaultMessage": "이 섹션의 변경을 방지하려면 잠그세요" }, + "course.assessment.generation.mrq.exportDialogHeader": { + "defaultMessage": "문항 내보내기 ({exportCount}개 선택됨)" + }, + "course.assessment.generation.mrq.numberOfQuestionsField": { + "defaultMessage": "문항 수" + }, + "course.assessment.generation.mrq.numberOfQuestionsRange": { + "defaultMessage": "{min}에서 {max} 사이의 숫자를 입력하세요" + }, "course.assessment.generation.newTab": { "defaultMessage": "새로 만들기" }, "course.assessment.generation.openExportDialog": { "defaultMessage": "내보내기" }, + "course.assessment.generation.promptPlaceholder": { + "defaultMessage": "여기에 입력하세요..." + }, + "course.assessment.generation.requireNonEmptyOptionError": { + "defaultMessage": "문항에는 최소 하나의 빈칸이 아닌 선택지가 있어야 합니다" + }, "course.assessment.generation.resetConversation": { "defaultMessage": "초기화" }, + "course.assessment.generation.showInactive": { + "defaultMessage": "비활성 항목 보기" + }, "course.assessment.generation.unlockTooltip": { "defaultMessage": "이 섹션을 계속 편집하려면 잠금을 해제하세요" }, - "course.assessment.generation.mrq.numberOfQuestionsField": { - "defaultMessage": "문항 수" - }, - "course.assessment.generation.promptPlaceholder": { - "defaultMessage": "여기에 입력하세요..." - }, - "course.assessment.generation.generateQuestion": { - "defaultMessage": "생성" - }, - "course.assessment.generation.showInactive": { - "defaultMessage": "비활성 항목 보기" - }, - "course.assessment.generation.mrq.numberOfQuestionsRange": { - "defaultMessage": "{min}에서 {max} 사이의 숫자를 입력하세요" - }, - "course.assessment.generation.enhanceMode": { - "defaultMessage": "강화" - }, - "course.assessment.generation.createMode": { - "defaultMessage": "새로 만들기" - }, - "course.assessment.generation.enhanceModeTooltip": { - "defaultMessage": "현재 질문을 기반으로 확장합니다" - }, - "course.assessment.generation.createModeTooltip": { - "defaultMessage": "처음부터 새로운 질문을 생성합니다" - }, - "course.assessment.generation.mrq.exportDialogHeader": { - "defaultMessage": "문항 내보내기 ({exportCount}개 선택됨)" - }, - "course.assessment.generation.requireNonEmptyOptionError": { - "defaultMessage": "문항에는 최소 하나의 빈칸이 아닌 선택지가 있어야 합니다" - }, "course.assessment.generation.untitledQuestion": { "defaultMessage": "제목 없는 문항" }, - "course.assessment.question.multipleResponses.showOptions": { - "defaultMessage": "옵션 보기" - }, - "course.assessment.question.multipleResponses.hideOptions": { - "defaultMessage": "옵션 숨기기" - }, - "course.assessment.question.multipleResponses.noOptions": { - "defaultMessage": "옵션 없음" - }, - "course.assessment.question.multipleResponses.title": { - "defaultMessage": "제목" - }, - "course.assessment.generation.generateMrqPage": { - "defaultMessage": "다중 선택 문항 생성" - }, - "course.assessment.generation.generateMcqPage": { - "defaultMessage": "객관식 문항 생성" - }, - "course.assessment.generation.generateMultipleSuccess": { - "defaultMessage": "{count}개의 문항이 성공적으로 생성되었습니다!" - }, - "course.assessment.generation.generateSuccess": { - "defaultMessage": "{title} 생성에 성공했습니다." - }, - "course.assessment.generation.generateError": { - "defaultMessage": "{title} 문항 생성 중 오류가 발생했습니다." - }, - "course.assessment.generation.loadingSourceError": { - "defaultMessage": "원본 문항 데이터를 불러올 수 없습니다." - }, - "course.assessment.generation.allFieldsLocked": { - "defaultMessage": "모든 필드가 잠겨 있어 생성을 진행할 수 없습니다." - }, "course.assessment.liveFeedback.comments": { "defaultMessage": "댓글" }, - "course.assessment.liveFeedback.messageTimingTitle": { - "defaultMessage": "{usedAt}에 생성됨" - }, "course.assessment.liveFeedback.lineHeader": { "defaultMessage": "{lineNumber} 줄" }, - "course.assessment.submission.GetHelpChatPage.chatInputText": { - "defaultMessage": "어떻게 도와드릴까요?" - }, - "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { - "defaultMessage": "{numMessages} / {maxMessages} 개 메시지 남음" - }, - "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { - "defaultMessage": "이 질문의 메시지 한도에 도달했습니다." - }, - "course.assessment.submission.GetHelpChatPage.codeUpdated": { - "defaultMessage": "코드가 업데이트되었습니다" - }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { - "defaultMessage": "{lineNumber} 줄" - }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { - "defaultMessage": "{filename}:{lineNumber}" - }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { - "defaultMessage": "위의 채팅이 종료되었습니다. 새 채팅을 시작하시겠습니까?" - }, "course.assessment.liveFeedback.liveFeedbackName": { "defaultMessage": "라이브 피드백" }, + "course.assessment.liveFeedback.messageTimingTitle": { + "defaultMessage": "{usedAt}에 생성됨" + }, "course.assessment.liveFeedback.questionTitle": { "defaultMessage": "질문 {index}" }, @@ -1718,6 +1697,54 @@ "course.assessment.newAssessment": { "defaultMessage": "새 평가" }, + "course.assessment.plagiarism.actions": { + "defaultMessage": "작업" + }, + "course.assessment.plagiarism.baseSubmission": { + "defaultMessage": "기본 제출" + }, + "course.assessment.plagiarism.comparedSubmission": { + "defaultMessage": "비교된 제출" + }, + "course.assessment.plagiarism.confirmStartMessage": { + "defaultMessage": "새 표절 검사를 실행하면 이전 결과가 제거됩니다." + }, + "course.assessment.plagiarism.confirmStartTitle": { + "defaultMessage": "표절 검사 확인?" + }, + "course.assessment.plagiarism.downloadPdf": { + "defaultMessage": "PDF 다운로드" + }, + "course.assessment.plagiarism.lastRunTime": { + "defaultMessage": "마지막 실행 시간: {date}" + }, + "course.assessment.plagiarism.notStarted": { + "defaultMessage": "표절 검사가 실행되지 않았습니다." + }, + "course.assessment.plagiarism.plagiarism": { + "defaultMessage": "표절 결과" + }, + "course.assessment.plagiarism.results": { + "defaultMessage": "표절 결과 (제출물 간 유사도)" + }, + "course.assessment.plagiarism.searchByStudentName": { + "defaultMessage": "학생 이름으로 검색" + }, + "course.assessment.plagiarism.showSelfPlagiarism": { + "defaultMessage": "자기 표절 비교 포함 (동일 학생, 다른 과정)" + }, + "course.assessment.plagiarism.similarityScore": { + "defaultMessage": "유사성 점수" + }, + "course.assessment.plagiarism.start": { + "defaultMessage": "새 표절 검사" + }, + "course.assessment.plagiarism.status": { + "defaultMessage": "표절 검사 상태" + }, + "course.assessment.plagiarism.viewReport": { + "defaultMessage": "보고서 보기" + }, "course.assessment.question.forumPostResponses.enableTextResponse": { "defaultMessage": "학생들이 추가 입력을 제공할 수 있도록 텍스트 필드 포함" }, @@ -1790,6 +1817,9 @@ "course.assessment.question.multipleResponses.grading": { "defaultMessage": "채점" }, + "course.assessment.question.multipleResponses.hideOptions": { + "defaultMessage": "옵션 숨기기" + }, "course.assessment.question.multipleResponses.ignoresRandomization": { "defaultMessage": "무작위화 무시" }, @@ -1826,6 +1856,9 @@ "course.assessment.question.multipleResponses.newResponseCannotUndo": { "defaultMessage": "이것은 새로운 응답입니다. 저장하기 전에 삭제하면 즉시 사라집니다." }, + "course.assessment.question.multipleResponses.noOptions": { + "defaultMessage": "옵션 없음" + }, "course.assessment.question.multipleResponses.noSkillsCanCreateSkills": { "defaultMessage": "이 강좌에는 아직 기술이 없습니다. 기술 페이지에서 새 기술을 생성할 수 있습니다." }, @@ -1862,6 +1895,9 @@ "course.assessment.question.multipleResponses.saveChangesFirstBeforeConvertingMcqMrq": { "defaultMessage": "이 질문을 변환하기 전에 변경 사항을 저장하세요." }, + "course.assessment.question.multipleResponses.showOptions": { + "defaultMessage": "옵션 보기" + }, "course.assessment.question.multipleResponses.skills": { "defaultMessage": "기술" }, @@ -1874,6 +1910,9 @@ "course.assessment.question.multipleResponses.staffOnlyCommentsHint": { "defaultMessage": "내부 메모 또는 문서화에 유용합니다. 학생들은 절대 볼 수 없습니다." }, + "course.assessment.question.multipleResponses.title": { + "defaultMessage": "제목" + }, "course.assessment.question.multipleResponses.undoDeleteChoice": { "defaultMessage": "삭제 취소 선택" }, @@ -1949,12 +1988,12 @@ "course.assessment.question.programming.defaultEvaluator": { "defaultMessage": "기본" }, - "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { - "defaultMessage": "{name}: 설치된 종속성" - }, "course.assessment.question.programming.defaultEvaluatorDependencyDescription": { "defaultMessage": "제출된 코드는 컨테이너화된 환경에서 실행되며, 다음 종속성이 로컬에 설치되어 있습니다.{br}프로그래밍 문제에 아래에 없는 종속성이 필요하다면, 문의해 주세요. 추가 여부를 검토하겠습니다." }, + "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { + "defaultMessage": "{name}: 설치된 종속성" + }, "course.assessment.question.programming.defaultEvaluatorHint": { "defaultMessage": "아무 문제 없이, 아래 평가 패키지에 따라 코드를 실행하고 테스트 결과를 보고합니다." }, @@ -2087,24 +2126,24 @@ "course.assessment.question.programming.packageCreationModeHint": { "defaultMessage": "이 질문이 성공적으로 생성되면 이 모드를 변경할 수 없습니다. 신중하게 선택하세요!" }, - "course.assessment.question.programming.packageImportSuccess": { - "defaultMessage": "패키지가 성공적으로 가져와졌습니다." + "course.assessment.question.programming.packageImportEvaluationError": { + "defaultMessage": "솔루션을 테스트 케이스와 비교하는 과정에서 오류가 발생했습니다. 다시 한 번 확인하고 시도해주세요." + }, + "course.assessment.question.programming.packageImportEvaluationTimeout": { + "defaultMessage": "요구된 시간 내에 평가자로부터 응답을 받지 못했습니다. 현재 모든 평가자가 사용 중일 수 있습니다. 나중에 다시 시도해주세요." + }, + "course.assessment.question.programming.packageImportGenericError": { + "defaultMessage": "패키지를 가져올 수 없습니다: {error}" }, "course.assessment.question.programming.packageImportInvalidPackage": { "defaultMessage": "패키지를 가져올 수 없습니다: 업로드된 패키지의 구조가 올바르지 않습니다." }, - "course.assessment.question.programming.packageImportEvaluationTimeout": { - "defaultMessage": "요구된 시간 내에 평가자로부터 응답을 받지 못했습니다. 현재 모든 평가자가 사용 중일 수 있습니다. 나중에 다시 시도해주세요." + "course.assessment.question.programming.packageImportSuccess": { + "defaultMessage": "패키지가 성공적으로 가져와졌습니다." }, "course.assessment.question.programming.packageImportTimeLimitExceeded": { "defaultMessage": "지정된 시간 제한 내에 테스트 케이스 평가를 완료하지 못했습니다." }, - "course.assessment.question.programming.packageImportEvaluationError": { - "defaultMessage": "솔루션을 테스트 케이스와 비교하는 과정에서 오류가 발생했습니다. 다시 한 번 확인하고 시도해주세요." - }, - "course.assessment.question.programming.packageImportGenericError": { - "defaultMessage": "패키지를 가져올 수 없습니다: {error}" - }, "course.assessment.question.programming.packageInfoOnline": { "defaultMessage": "생성된 평가 패키지" }, @@ -2207,155 +2246,197 @@ "course.assessment.question.programminquestion.questionSavedRedirecting": { "defaultMessage": "질문이 저장되었습니다." }, - "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { - "defaultMessage": "공란으로 둘 수 없습니다." + "course.assessment.question.rubricPlayground.add": { + "defaultMessage": "추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.chooseFileButton": { - "defaultMessage": "파일 선택" + "course.assessment.question.rubricPlayground.addExistingAnswers": { + "defaultMessage": "기존 답변 추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.descriptionFieldLabel": { - "defaultMessage": "설명" + "course.assessment.question.rubricPlayground.addNewCategory": { + "defaultMessage": "새 카테고리 추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.fetchFailureMessage": { - "defaultMessage": "오류가 발생했습니다. 다시 시도하세요." + "course.assessment.question.rubricPlayground.addNewGrade": { + "defaultMessage": "새 점수 추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.fileAttachmentRequired": { - "defaultMessage": "파일 첨부 필요" + "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { + "defaultMessage": "{inputComponent}개의 무작위 학생 답변 추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.fileUploaded": { - "defaultMessage": "업로드된 파일:" + "course.assessment.question.rubricPlayground.addSampleAnswers": { + "defaultMessage": "샘플 답변 추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.lessThanEqualZeroValidationError": { - "defaultMessage": "값은 0보다 커야 합니다." + "course.assessment.question.rubricPlayground.addSampleAnswersTitle": { + "defaultMessage": "샘플 답변 추가" }, - "course.assessment.question.scribing.ScribingQuestionForm.maximumGradeFieldLabel": { - "defaultMessage": "최대 등급" + "course.assessment.question.rubricPlayground.answer": { + "defaultMessage": "답변" }, - "course.assessment.question.scribing.ScribingQuestionForm.noFileChosenMessage": { - "defaultMessage": "선택된 파일이 없습니다" + "course.assessment.question.rubricPlayground.apply": { + "defaultMessage": "적용" }, - "course.assessment.question.scribing.ScribingQuestionForm.positiveNumberValidationError": { - "defaultMessage": "값은 양수여야 합니다." + "course.assessment.question.rubricPlayground.applyFailure": { + "defaultMessage": "채점 결과 적용 실패" }, - "course.assessment.question.scribing.ScribingQuestionForm.resolveErrorsMessage": { - "defaultMessage": "이 양식에 오류가 있습니다. 제출 전에 해결하세요." + "course.assessment.question.rubricPlayground.applySuccess": { + "defaultMessage": "채점 루브릭, 프롬프트 및 결과가 성공적으로 적용되었습니다." }, - "course.assessment.question.scribing.ScribingQuestionForm.scribingQuestionWarning": { - "defaultMessage": "참고: PDF 파일의 각 페이지는 동일한 질문 세부사항을 갖는 단일 스크라이빙 질문으로 생성됩니다. 선택 입력을 비워 두고 생성 후 질문을 다시 편집할 수 있습니다." + "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { + "defaultMessage": "이 루브릭을 적용하면 이 페이지에서 아직 평가되지 않은 답변을 포함하여 모든 학생 답변에 점수가 할당됩니다." }, - "course.assessment.question.scribing.ScribingQuestionForm.skillsFieldLabel": { - "defaultMessage": "기술" + "course.assessment.question.rubricPlayground.applyingRubricGradingData": { + "defaultMessage": "루브릭 채점 데이터 적용 중..." }, - "course.assessment.question.scribing.ScribingQuestionForm.staffOnlyCommentsFieldLabel": { - "defaultMessage": "직원 전용 코멘트" + "course.assessment.question.rubricPlayground.categoryHeading": { + "defaultMessage": "카{index}" }, - "course.assessment.question.scribing.ScribingQuestionForm.submitButton": { - "defaultMessage": "제출" + "course.assessment.question.rubricPlayground.categoryName": { + "defaultMessage": "카테고리 이름" }, - "course.assessment.question.scribing.ScribingQuestionForm.submitFailureMessage": { - "defaultMessage": "오류가 발생했습니다. 다시 시도하세요." + "course.assessment.question.rubricPlayground.compare": { + "defaultMessage": "비교" }, - "course.assessment.question.scribing.ScribingQuestionForm.submittingMessage": { - "defaultMessage": "제출 중..." + "course.assessment.question.rubricPlayground.comparingRevisions": { + "defaultMessage": "{count}개 개정판 비교 중" }, - "course.assessment.question.scribing.ScribingQuestionForm.titleFieldLabel": { - "defaultMessage": "제목" + "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { + "defaultMessage": "AI 채점 적용 확인" }, - "course.assessment.question.scribing.ScribingQuestionForm.valueMoreThan1000Error": { - "defaultMessage": "값은 1000보다 작아야 합니다." + "course.assessment.question.rubricPlayground.confirmProceed": { + "defaultMessage": "계속 진행하시겠습니까?" }, - "course.assessment.question.textResponses.addSolution": { - "defaultMessage": "새 솔루션 추가" + "course.assessment.question.rubricPlayground.dismiss": { + "defaultMessage": "닫기" }, - "course.assessment.question.textResponses.allowFileUpload": { - "defaultMessage": "답변에서 파일 업로드 허용" + "course.assessment.question.rubricPlayground.evaluate": { + "defaultMessage": "평가" }, - "course.assessment.question.textResponses.deleteSolution": { - "defaultMessage": "솔루션 삭제" + "course.assessment.question.rubricPlayground.evaluateAll": { + "defaultMessage": "모두 평가 ({count})" }, - "course.assessment.question.textResponses.exactMatch": { - "defaultMessage": "정확히 일치" + "course.assessment.question.rubricPlayground.evaluateRemaining": { + "defaultMessage": "나머지 평가 ({count})" }, - "course.assessment.question.textResponses.fileUploadNote": { - "defaultMessage": "참고: 파일 업로드 질문은 자동 채점이 불가능합니다. 자동 채점기는 항상 최대 등급을 부여합니다." + "course.assessment.question.rubricPlayground.evaluating": { + "defaultMessage": "평가 중" }, - "course.assessment.question.textResponses.noSolutionsNote": { - "defaultMessage": "솔루션이 없을 경우, 자동 채점기는 항상 최대 점수를 부여합니다." + "course.assessment.question.rubricPlayground.feedback": { + "defaultMessage": "피드백" }, - "course.assessment.question.textResponses.atLeastOneSolutionNote": { - "defaultMessage": "하나 이상의 솔루션이 제공된 경우, 학생들은 일반 텍스트만 입력할 수 있습니다." + "course.assessment.question.rubricPlayground.gradingCategories": { + "defaultMessage": "채점 카테고리" }, - "course.assessment.question.textResponses.exactMatchSolutionNote": { - "defaultMessage": "정확히 일치를 선택한 경우, 여러 줄의 솔루션은 학생의 답변과 정확히 일치해야 정답으로 채점됩니다. 이러한 일치가 발견된 경우, 자동 채점기는 다른 솔루션을 고려하지 않고 지정된 점수를 부여합니다." + "course.assessment.question.rubricPlayground.gradingPrompt": { + "defaultMessage": "채점 프롬프트" }, - "course.assessment.question.textResponses.solutionGradesExceedMaximumGradeNote": { - "defaultMessage": "학생의 답변이 여러 솔루션을 충족하는 경우, 자동 채점기는 문제의 최대 점수까지 해당 솔루션들의 점수 합계를 부여합니다." + "course.assessment.question.rubricPlayground.gradingPromptDescription": { + "defaultMessage": "채점 및 피드백 제공 시 AI를 안내하는 지침입니다." }, - "course.assessment.question.textResponses.grade": { - "defaultMessage": "등급" + "course.assessment.question.rubricPlayground.max": { + "defaultMessage": "최대" }, - "course.assessment.question.textResponses.keyword": { - "defaultMessage": "키워드" + "course.assessment.question.rubricPlayground.modelAnswer": { + "defaultMessage": "모범 답안" }, - "course.assessment.question.textResponses.newSolutionCannotUndo": { - "defaultMessage": "이것은 새 솔루션입니다. 저장하기 전에 삭제하면 즉시 사라집니다." + "course.assessment.question.rubricPlayground.modelAnswerDescription": { + "defaultMessage": "각 카테고리에서 최대 점수를 받는 예시입니다." }, - "course.assessment.question.textResponses.solution": { - "defaultMessage": "솔루션" + "course.assessment.question.rubricPlayground.noAnswers": { + "defaultMessage": "샘플 답변이 추가되지 않았습니다. 시작하려면 추가하세요." }, - "course.assessment.question.textResponses.solutionType": { - "defaultMessage": "솔루션 유형" + "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { + "defaultMessage": "이 페이지에 저장된 최신 개정판이 아닌 루브릭을 적용하도록 선택했습니다." }, - "course.assessment.question.textResponses.solutionWillBeDeleted": { - "defaultMessage": "변경을 저장하면 이 솔루션이 삭제됩니다." + "course.assessment.question.rubricPlayground.questionGrade": { + "defaultMessage": "점수" }, - "course.assessment.question.textResponses.solutions": { - "defaultMessage": "솔루션" + "course.assessment.question.rubricPlayground.reevaluate": { + "defaultMessage": "재평가" }, - "course.assessment.question.textResponses.solutionsHint": { - "defaultMessage": "솔루션을 추가하면 학생의 답변을 자동으로 채점할 수 있습니다." + "course.assessment.question.rubricPlayground.reevaluateAll": { + "defaultMessage": "모두 재평가 ({count})" }, - "course.assessment.question.textResponses.undoDeleteSolution": { - "defaultMessage": "솔루션 삭제 취소" + "course.assessment.question.rubricPlayground.rubricPlayground": { + "defaultMessage": "루브릭 플레이그라운드" }, - "course.assessment.question.textResponses.zeroGrade": { - "defaultMessage": "0.0" + "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { + "defaultMessage": "샘플 답변 평가" }, - "course.assessment.question.textResponses.templateText": { - "defaultMessage": "템플릿" + "course.assessment.question.rubricPlayground.savedRubric": { + "defaultMessage": "저장된 루브릭, {date}" }, - "course.assessment.question.textResponses.templateTextDescription": { - "defaultMessage": "학생이 이 문제를 처음 시도할 때 답변 영역에 표시되는 텍스트입니다." + "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { + "defaultMessage": "학생 이름 또는 점수로 답변 검색" }, - "course.assessment.question.textResponses.regex": { - "defaultMessage": "정규 표현식" + "course.assessment.question.rubricPlayground.student": { + "defaultMessage": "학생" }, - "course.assessment.question.textResponses.invalidRegex": { - "defaultMessage": "유효하지 않은 정규 표현식입니다" + "course.assessment.question.rubricPlayground.totalGrade": { + "defaultMessage": "총점" }, - "course.assessment.question.textResponses.spreadsheetFormula": { - "defaultMessage": "스프레드시트 공식" + "course.assessment.question.rubricPlayground.viewEditRubric": { + "defaultMessage": "루브릭 보기 / 편집" }, - "course.assessment.question.textResponses.testSpreadsheet": { - "defaultMessage": "테스트 스프레드시트" + "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { + "defaultMessage": "여기에 답변을 작성하세요" }, - "course.assessment.question.textResponses.testSpreadsheetDescription": { - "defaultMessage": "학생 답변을 자동 채점하려면 테스트 스프레드시트가 필요합니다." + "course.assessment.question.rubricPlayground.writeCustomAnswer": { + "defaultMessage": "사용자 정의 답변 작성" }, - "course.assessment.question.textResponses.testSpreadsheetRequired": { - "defaultMessage": "테스트 스프레드시트 파일을 업로드해 주세요." + "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { + "defaultMessage": "공란으로 둘 수 없습니다." }, - "course.assessment.question.textResponses.spreadsheetAdvancedOptions": { - "defaultMessage": "고급 옵션" + "course.assessment.question.scribing.ScribingQuestionForm.chooseFileButton": { + "defaultMessage": "파일 선택" }, - "course.assessment.question.textResponses.spreadsheetRandomization": { - "defaultMessage": "무작위화 활성화" + "course.assessment.question.scribing.ScribingQuestionForm.descriptionFieldLabel": { + "defaultMessage": "설명" }, - "course.assessment.question.textResponses.spreadsheetRandomizationDescription": { - "defaultMessage": "활성화하면 자동 채점 시 일부 스프레드시트 값이 무작위로 조정됩니다. 이는 공식이 사전 설정된 값 대신 올바른 답을 실제로 계산하는지 확인하는 데 유용합니다." + "course.assessment.question.scribing.ScribingQuestionForm.fetchFailureMessage": { + "defaultMessage": "오류가 발생했습니다. 다시 시도하세요." }, - "course.assessment.question.textResponses.numberOfRandomTests": { - "defaultMessage": "무작위 테스트 횟수" + "course.assessment.question.scribing.ScribingQuestionForm.fileAttachmentRequired": { + "defaultMessage": "파일 첨부 필요" + }, + "course.assessment.question.scribing.ScribingQuestionForm.fileUploaded": { + "defaultMessage": "업로드된 파일:" + }, + "course.assessment.question.scribing.ScribingQuestionForm.lessThanEqualZeroValidationError": { + "defaultMessage": "값은 0보다 커야 합니다." + }, + "course.assessment.question.scribing.ScribingQuestionForm.maximumGradeFieldLabel": { + "defaultMessage": "최대 등급" + }, + "course.assessment.question.scribing.ScribingQuestionForm.noFileChosenMessage": { + "defaultMessage": "선택된 파일이 없습니다" + }, + "course.assessment.question.scribing.ScribingQuestionForm.positiveNumberValidationError": { + "defaultMessage": "값은 양수여야 합니다." + }, + "course.assessment.question.scribing.ScribingQuestionForm.resolveErrorsMessage": { + "defaultMessage": "이 양식에 오류가 있습니다. 제출 전에 해결하세요." + }, + "course.assessment.question.scribing.ScribingQuestionForm.scribingQuestionWarning": { + "defaultMessage": "참고: PDF 파일의 각 페이지는 동일한 질문 세부사항을 갖는 단일 스크라이빙 질문으로 생성됩니다. 선택 입력을 비워 두고 생성 후 질문을 다시 편집할 수 있습니다." + }, + "course.assessment.question.scribing.ScribingQuestionForm.skillsFieldLabel": { + "defaultMessage": "기술" + }, + "course.assessment.question.scribing.ScribingQuestionForm.staffOnlyCommentsFieldLabel": { + "defaultMessage": "직원 전용 코멘트" + }, + "course.assessment.question.scribing.ScribingQuestionForm.submitButton": { + "defaultMessage": "제출" + }, + "course.assessment.question.scribing.ScribingQuestionForm.submitFailureMessage": { + "defaultMessage": "오류가 발생했습니다. 다시 시도하세요." + }, + "course.assessment.question.scribing.ScribingQuestionForm.submittingMessage": { + "defaultMessage": "제출 중..." + }, + "course.assessment.question.scribing.ScribingQuestionForm.titleFieldLabel": { + "defaultMessage": "제목" + }, + "course.assessment.question.scribing.ScribingQuestionForm.valueMoreThan1000Error": { + "defaultMessage": "값은 1000보다 작아야 합니다." }, "course.assessment.question.textResponses.SpreadsheetManager.fixedRandomSeed": { "defaultMessage": "고정 무작위 시드" @@ -2369,24 +2450,54 @@ "course.assessment.question.textResponses.SpreadsheetManager.fixedTimestampDescription": { "defaultMessage": "체크하면 현재 날짜/시간을 사용하는 공식(NOW, TODAY 등)이 이 특정 날짜/시간을 대신 사용합니다." }, - "course.assessment.question.textResponses.randomizationMode": { - "defaultMessage": "자동 채점 값 구성" + "course.assessment.question.textResponses.addSolution": { + "defaultMessage": "새 솔루션 추가" }, - "course.assessment.question.textResponses.randomizationModeDescription": { - "defaultMessage": "자동 채점 시 값을 어떻게 대체하거나 무작위화할지 설정하려면 셀을 클릭하세요." + "course.assessment.question.textResponses.allowFileUpload": { + "defaultMessage": "답변에서 파일 업로드 허용" }, - "course.assessment.question.textResponses.randomizationModeClearAll": { - "defaultMessage": "모두 지우기" + "course.assessment.question.textResponses.atLeastOneSolutionNote": { + "defaultMessage": "하나 이상의 솔루션이 제공된 경우, 학생들은 일반 텍스트만 입력할 수 있습니다." }, - "course.assessment.question.textResponses.randomizationModeRestoreDefaults": { - "defaultMessage": "기본값 복원" + "course.assessment.question.textResponses.dateRandomizationMode": { + "defaultMessage": "무작위 날짜 값 사용" }, - "course.assessment.question.textResponses.randomizationModePopoverTitle": { - "defaultMessage": "자동 채점 중..." + "course.assessment.question.textResponses.deleteSolution": { + "defaultMessage": "솔루션 삭제" + }, + "course.assessment.question.textResponses.exactMatch": { + "defaultMessage": "정확히 일치" + }, + "course.assessment.question.textResponses.exactMatchSolutionNote": { + "defaultMessage": "정확히 일치를 선택한 경우, 여러 줄의 솔루션은 학생의 답변과 정확히 일치해야 정답으로 채점됩니다. 이러한 일치가 발견된 경우, 자동 채점기는 다른 솔루션을 고려하지 않고 지정된 점수를 부여합니다." + }, + "course.assessment.question.textResponses.fileUploadNote": { + "defaultMessage": "참고: 파일 업로드 질문은 자동 채점이 불가능합니다. 자동 채점기는 항상 최대 등급을 부여합니다." + }, + "course.assessment.question.textResponses.grade": { + "defaultMessage": "등급" + }, + "course.assessment.question.textResponses.invalidRegex": { + "defaultMessage": "유효하지 않은 정규 표현식입니다" + }, + "course.assessment.question.textResponses.keyword": { + "defaultMessage": "키워드" + }, + "course.assessment.question.textResponses.newSolutionCannotUndo": { + "defaultMessage": "이것은 새 솔루션입니다. 저장하기 전에 삭제하면 즉시 사라집니다." }, "course.assessment.question.textResponses.noRandomizationMode": { "defaultMessage": "원래 값 유지" }, + "course.assessment.question.textResponses.noSolutionsNote": { + "defaultMessage": "솔루션이 없을 경우, 자동 채점기는 항상 최대 점수를 부여합니다." + }, + "course.assessment.question.textResponses.numberOfRandomTests": { + "defaultMessage": "무작위 테스트 횟수" + }, + "course.assessment.question.textResponses.numericRandomizationMode": { + "defaultMessage": "무작위 숫자 값 사용" + }, "course.assessment.question.textResponses.overrideRandomizationMode": { "defaultMessage": "지정된 값으로 덮어쓰기" }, @@ -2396,17 +2507,20 @@ "course.assessment.question.textResponses.overrideValue": { "defaultMessage": "값" }, - "course.assessment.question.textResponses.numericRandomizationMode": { - "defaultMessage": "무작위 숫자 값 사용" + "course.assessment.question.textResponses.randomizationMode": { + "defaultMessage": "자동 채점 값 구성" }, - "course.assessment.question.textResponses.roundToInteger": { - "defaultMessage": "정수로 반올림" + "course.assessment.question.textResponses.randomizationModeClearAll": { + "defaultMessage": "모두 지우기" }, - "course.assessment.question.textResponses.stringRandomizationMode": { - "defaultMessage": "값의 문자 무작위화" + "course.assessment.question.textResponses.randomizationModeDescription": { + "defaultMessage": "자동 채점 시 값을 어떻게 대체하거나 무작위화할지 설정하려면 셀을 클릭하세요." }, - "course.assessment.question.textResponses.stringRandomizationModeDescription": { - "defaultMessage": "무작위화할 각 문자 클래스는 같은 클래스의 다른 문자로 대체될 수 있습니다." + "course.assessment.question.textResponses.randomizationModePopoverTitle": { + "defaultMessage": "자동 채점 중..." + }, + "course.assessment.question.textResponses.randomizationModeRestoreDefaults": { + "defaultMessage": "기본값 복원" }, "course.assessment.question.textResponses.randomizeDigits": { "defaultMessage": "숫자 무작위화 (0-9)" @@ -2414,152 +2528,77 @@ "course.assessment.question.textResponses.randomizeLetters": { "defaultMessage": "문자 무작위화 (a-z)" }, - "course.assessment.question.textResponses.dateRandomizationMode": { - "defaultMessage": "무작위 날짜 값 사용" + "course.assessment.question.textResponses.regex": { + "defaultMessage": "정규 표현식" }, "course.assessment.question.textResponses.roundToDay": { "defaultMessage": "일 단위로 내림 (00:00 UTC)" }, + "course.assessment.question.textResponses.roundToInteger": { + "defaultMessage": "정수로 반올림" + }, "course.assessment.question.textResponses.shuffleRandomizationMode": { "defaultMessage": "다른 셀과 값 섞기" }, "course.assessment.question.textResponses.shuffleRandomizationModeDescription": { "defaultMessage": "\"섞기\"로 표시된 각 셀의 값은 무작위로 재배치됩니다. 전체 분포는 동일하게 유지되지만 각 값은 다른 셀에 배치될 수 있습니다." }, - "course.assessment.question.rubricPlayground.rubricPlayground": { - "defaultMessage": "루브릭 플레이그라운드" - }, - "course.assessment.question.rubricPlayground.savedRubric": { - "defaultMessage": "저장된 루브릭, {date}" + "course.assessment.question.textResponses.solution": { + "defaultMessage": "솔루션" }, - "course.assessment.question.rubricPlayground.viewEditRubric": { - "defaultMessage": "루브릭 보기 / 편집" + "course.assessment.question.textResponses.solutionGradesExceedMaximumGradeNote": { + "defaultMessage": "학생의 답변이 여러 솔루션을 충족하는 경우, 자동 채점기는 문제의 최대 점수까지 해당 솔루션들의 점수 합계를 부여합니다." }, - "course.assessment.question.rubricPlayground.evaluate": { - "defaultMessage": "평가" + "course.assessment.question.textResponses.solutionType": { + "defaultMessage": "솔루션 유형" }, - "course.assessment.question.rubricPlayground.compare": { - "defaultMessage": "비교" - }, - "course.assessment.question.rubricPlayground.apply": { - "defaultMessage": "적용" - }, - "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { - "defaultMessage": "AI 채점 적용 확인" - }, - "course.assessment.question.rubricPlayground.applyingRubricGradingData": { - "defaultMessage": "루브릭 채점 데이터 적용 중..." - }, - "course.assessment.question.rubricPlayground.applySuccess": { - "defaultMessage": "채점 루브릭, 프롬프트 및 결과가 성공적으로 적용되었습니다." - }, - "course.assessment.question.rubricPlayground.applyFailure": { - "defaultMessage": "채점 결과 적용 실패" - }, - "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { - "defaultMessage": "이 페이지에 저장된 최신 개정판이 아닌 루브릭을 적용하도록 선택했습니다." - }, - "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { - "defaultMessage": "이 루브릭을 적용하면 이 페이지에서 아직 평가되지 않은 답변을 포함하여 모든 학생 답변에 점수가 할당됩니다." - }, - "course.assessment.question.rubricPlayground.confirmProceed": { - "defaultMessage": "계속 진행하시겠습니까?" - }, - "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { - "defaultMessage": "샘플 답변 평가" - }, - "course.assessment.question.rubricPlayground.addSampleAnswers": { - "defaultMessage": "샘플 답변 추가" - }, - "course.assessment.question.rubricPlayground.evaluateAll": { - "defaultMessage": "모두 평가 ({count})" - }, - "course.assessment.question.rubricPlayground.reevaluateAll": { - "defaultMessage": "모두 재평가 ({count})" - }, - "course.assessment.question.rubricPlayground.evaluateRemaining": { - "defaultMessage": "나머지 평가 ({count})" - }, - "course.assessment.question.rubricPlayground.comparingRevisions": { - "defaultMessage": "{count}개 개정판 비교 중" - }, - "course.assessment.question.rubricPlayground.addSampleAnswersTitle": { - "defaultMessage": "샘플 답변 추가" - }, - "course.assessment.question.rubricPlayground.add": { - "defaultMessage": "추가" - }, - "course.assessment.question.rubricPlayground.addExistingAnswers": { - "defaultMessage": "기존 답변 추가" - }, - "course.assessment.question.rubricPlayground.student": { - "defaultMessage": "학생" - }, - "course.assessment.question.rubricPlayground.questionGrade": { - "defaultMessage": "점수" - }, - "course.assessment.question.rubricPlayground.categoryHeading": { - "defaultMessage": "카{index}" - }, - "course.assessment.question.rubricPlayground.answer": { - "defaultMessage": "답변" - }, - "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { - "defaultMessage": "학생 이름 또는 점수로 답변 검색" - }, - "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { - "defaultMessage": "{inputComponent}개의 무작위 학생 답변 추가" - }, - "course.assessment.question.rubricPlayground.writeCustomAnswer": { - "defaultMessage": "사용자 정의 답변 작성" - }, - "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { - "defaultMessage": "여기에 답변을 작성하세요" + "course.assessment.question.textResponses.solutionWillBeDeleted": { + "defaultMessage": "변경을 저장하면 이 솔루션이 삭제됩니다." }, - "course.assessment.question.rubricPlayground.dismiss": { - "defaultMessage": "닫기" + "course.assessment.question.textResponses.solutions": { + "defaultMessage": "솔루션" }, - "course.assessment.question.rubricPlayground.noAnswers": { - "defaultMessage": "샘플 답변이 추가되지 않았습니다. 시작하려면 추가하세요." + "course.assessment.question.textResponses.solutionsHint": { + "defaultMessage": "솔루션을 추가하면 학생의 답변을 자동으로 채점할 수 있습니다." }, - "course.assessment.question.rubricPlayground.reevaluate": { - "defaultMessage": "재평가" + "course.assessment.question.textResponses.spreadsheetAdvancedOptions": { + "defaultMessage": "고급 옵션" }, - "course.assessment.question.rubricPlayground.totalGrade": { - "defaultMessage": "총점" + "course.assessment.question.textResponses.spreadsheetFormula": { + "defaultMessage": "스프레드시트 공식" }, - "course.assessment.question.rubricPlayground.feedback": { - "defaultMessage": "피드백" + "course.assessment.question.textResponses.spreadsheetRandomization": { + "defaultMessage": "무작위화 활성화" }, - "course.assessment.question.rubricPlayground.evaluating": { - "defaultMessage": "평가 중" + "course.assessment.question.textResponses.spreadsheetRandomizationDescription": { + "defaultMessage": "활성화하면 자동 채점 시 일부 스프레드시트 값이 무작위로 조정됩니다. 이는 공식이 사전 설정된 값 대신 올바른 답을 실제로 계산하는지 확인하는 데 유용합니다." }, - "course.assessment.question.rubricPlayground.gradingPrompt": { - "defaultMessage": "채점 프롬프트" + "course.assessment.question.textResponses.stringRandomizationMode": { + "defaultMessage": "값의 문자 무작위화" }, - "course.assessment.question.rubricPlayground.gradingPromptDescription": { - "defaultMessage": "채점 및 피드백 제공 시 AI를 안내하는 지침입니다." + "course.assessment.question.textResponses.stringRandomizationModeDescription": { + "defaultMessage": "무작위화할 각 문자 클래스는 같은 클래스의 다른 문자로 대체될 수 있습니다." }, - "course.assessment.question.rubricPlayground.modelAnswer": { - "defaultMessage": "모범 답안" + "course.assessment.question.textResponses.templateText": { + "defaultMessage": "템플릿" }, - "course.assessment.question.rubricPlayground.modelAnswerDescription": { - "defaultMessage": "각 카테고리에서 최대 점수를 받는 예시입니다." + "course.assessment.question.textResponses.templateTextDescription": { + "defaultMessage": "학생이 이 문제를 처음 시도할 때 답변 영역에 표시되는 텍스트입니다." }, - "course.assessment.question.rubricPlayground.gradingCategories": { - "defaultMessage": "채점 카테고리" + "course.assessment.question.textResponses.testSpreadsheet": { + "defaultMessage": "테스트 스프레드시트" }, - "course.assessment.question.rubricPlayground.addNewCategory": { - "defaultMessage": "새 카테고리 추가" + "course.assessment.question.textResponses.testSpreadsheetDescription": { + "defaultMessage": "학생 답변을 자동 채점하려면 테스트 스프레드시트가 필요합니다." }, - "course.assessment.question.rubricPlayground.categoryName": { - "defaultMessage": "카테고리 이름" + "course.assessment.question.textResponses.testSpreadsheetRequired": { + "defaultMessage": "테스트 스프레드시트 파일을 업로드해 주세요." }, - "course.assessment.question.rubricPlayground.max": { - "defaultMessage": "최대" + "course.assessment.question.textResponses.undoDeleteSolution": { + "defaultMessage": "솔루션 삭제 취소" }, - "course.assessment.question.rubricPlayground.addNewGrade": { - "defaultMessage": "새 점수 추가" + "course.assessment.question.textResponses.zeroGrade": { + "defaultMessage": "0.0" }, "course.assessment.session.assessmentNotStarted": { "defaultMessage": "평가가 아직 시작되지 않았습니다. {startDate} 이후에 다시 방문하세요." @@ -2696,14 +2735,14 @@ "course.assessment.show.generate": { "defaultMessage": "질문 생성" }, - "course.assessment.show.generateTooltip": { - "defaultMessage": "Codaveri AI와 협업하여 문제를 생성하세요" + "course.assessment.show.generateFromProgrammingQuestion": { + "defaultMessage": "Codaveri AI로 유사한 질문 생성" }, "course.assessment.show.generateFromQuestion": { "defaultMessage": "AI로 유사한 질문 생성" }, - "course.assessment.show.generateFromProgrammingQuestion": { - "defaultMessage": "Codaveri AI로 유사한 질문 생성" + "course.assessment.show.generateTooltip": { + "defaultMessage": "Codaveri AI와 협업하여 문제를 생성하세요" }, "course.assessment.show.gradedTestCases": { "defaultMessage": "평가된 테스트 케이스" @@ -2718,7 +2757,7 @@ "defaultMessage": "아래 질문." }, "course.assessment.show.headsUpExistingSubmissions": { - "defaultMessage": "주의—기존 제출이 있습니다!" + "defaultMessage": "주의-기존 제출이 있습니다!" }, "course.assessment.show.hideOptions": { "defaultMessage": "옵션 숨기기" @@ -2837,15 +2876,15 @@ "course.assessment.show.showMcqMrqSolution": { "defaultMessage": "객관식/다중 응답형 질문 솔루션 보기" }, - "course.assessment.show.showRubricToStudents": { - "defaultMessage": "학생에게 루브릭 세부사항 표시" - }, "course.assessment.show.showMcqSubmitResult": { "defaultMessage": "객관식 질문 제출 결과 보기" }, "course.assessment.show.showOptions": { "defaultMessage": "옵션 보기" }, + "course.assessment.show.showRubricToStudents": { + "defaultMessage": "학생에게 루브릭 세부사항 표시" + }, "course.assessment.show.sureChangingQuestionType": { "defaultMessage": "이 질문 유형을 변경하시겠습니까?" }, @@ -2975,60 +3014,27 @@ "course.assessment.statistics.ancestorFail": { "defaultMessage": "이 평가의 이전 반복을 검색하지 못했습니다." }, - "course.assessment.statistics.ancestorStatisticsFail": { - "defaultMessage": "조상의 통계를 검색하지 못했습니다." - }, - "course.assessment.plagiarism.plagiarism": { - "defaultMessage": "표절 결과" - }, - "course.assessment.plagiarism.status": { - "defaultMessage": "표절 검사 상태" - }, - "course.assessment.plagiarism.lastRunTime": { - "defaultMessage": "마지막 실행 시간: {date}" - }, - "course.assessment.plagiarism.start": { - "defaultMessage": "새 표절 검사" - }, - "course.assessment.plagiarism.notStarted": { - "defaultMessage": "표절 검사가 실행되지 않았습니다." - }, - "course.assessment.plagiarism.confirmStartTitle": { - "defaultMessage": "표절 검사 확인?" - }, - "course.assessment.plagiarism.confirmStartMessage": { - "defaultMessage": "새 표절 검사를 실행하면 이전 결과가 제거됩니다." - }, - "course.assessment.plagiarism.results": { - "defaultMessage": "표절 결과 (제출물 간 유사도)" - }, - "course.assessment.plagiarism.baseSubmission": { - "defaultMessage": "기본 제출" - }, - "course.assessment.plagiarism.comparedSubmission": { - "defaultMessage": "비교된 제출" - }, - "course.assessment.plagiarism.similarityScore": { - "defaultMessage": "유사성 점수" - }, - "course.assessment.plagiarism.actions": { - "defaultMessage": "작업" + "course.assessment.statistics.ancestorSelect.current": { + "defaultMessage": "현재" }, - "course.assessment.plagiarism.viewReport": { - "defaultMessage": "보고서 보기" + "course.assessment.statistics.ancestorSelect.fromCourse": { + "defaultMessage": "{courseTitle}에서" }, - "course.assessment.plagiarism.downloadPdf": { - "defaultMessage": "PDF 다운로드" + "course.assessment.statistics.ancestorSelect.subtitle": { + "defaultMessage": "이 평가의 이전 버전과 비교:" }, - "course.assessment.plagiarism.searchByStudentName": { - "defaultMessage": "학생 이름으로 검색" + "course.assessment.statistics.ancestorSelect.title": { + "defaultMessage": "복제 기록" }, - "course.assessment.plagiarism.showSelfPlagiarism": { - "defaultMessage": "자기 표절 비교 포함 (동일 학생, 다른 과정)" + "course.assessment.statistics.ancestorStatisticsFail": { + "defaultMessage": "조상의 통계를 검색하지 못했습니다." }, "course.assessment.statistics.answers": { "defaultMessage": "답변" }, + "course.assessment.statistics.attemptCount": { + "defaultMessage": "시도 횟수" + }, "course.assessment.statistics.attempts.filename": { "defaultMessage": "{assessment}에 대한 질문별 시도 통계" }, @@ -3041,6 +3047,9 @@ "course.assessment.statistics.closePrompt": { "defaultMessage": "닫기" }, + "course.assessment.statistics.duplicationHistory": { + "defaultMessage": "복제 기록" + }, "course.assessment.statistics.fail": { "defaultMessage": "통계를 검색하지 못했습니다." }, @@ -3059,48 +3068,30 @@ "course.assessment.statistics.grader": { "defaultMessage": "평가자" }, + "course.assessment.statistics.gradesPerQuestion": { + "defaultMessage": "문제별 점수" + }, "course.assessment.statistics.grayCellLegend": { "defaultMessage": "미정 (질문은 자동 채점할 수 없음)" }, "course.assessment.statistics.group": { "defaultMessage": "그룹" }, - "course.assessment.statistics.ancestorSelect.current": { - "defaultMessage": "현재" - }, - "course.assessment.statistics.ancestorSelect.fromCourse": { - "defaultMessage": "{courseTitle}에서" - }, - "course.assessment.statistics.ancestorSelect.subtitle": { - "defaultMessage": "이 평가의 이전 버전과 비교:" - }, - "course.assessment.statistics.ancestorSelect.title": { - "defaultMessage": "복제 기록" - }, - "course.assessment.statistics.attemptCount": { - "defaultMessage": "시도 횟수" - }, - "course.assessment.statistics.duplicationHistory": { - "defaultMessage": "복제 기록" - }, - "course.assessment.statistics.gradesPerQuestion": { - "defaultMessage": "문제별 점수" + "course.assessment.statistics.header": { + "defaultMessage": "{title}에 대한 통계" }, "course.assessment.statistics.includePhantom": { "defaultMessage": "팬텀 학생 포함" }, - "course.assessment.statistics.liveFeedback": { - "defaultMessage": "도움 받기" - }, - "course.assessment.statistics.header": { - "defaultMessage": "{title}에 대한 통계" - }, "course.assessment.statistics.legendHigherusage": { "defaultMessage": "사용량이 많음" }, "course.assessment.statistics.legendLowerUsage": { "defaultMessage": "사용량이 적음" }, + "course.assessment.statistics.liveFeedback": { + "defaultMessage": "도움 받기" + }, "course.assessment.statistics.liveFeedback.filename": { "defaultMessage": "{assessment}에 대한 질문별 라이브 피드백 통계" }, @@ -3173,12 +3164,6 @@ "course.assessment.submission.Answer.rendererNotImplemented": { "defaultMessage": "이 질문 유형에 대한 디스플레이가 아직 구현되지 않았습니다." }, - "course.assessment.submission.answerTooLarge": { - "defaultMessage": "답변이 너무 큽니다" - }, - "course.assessment.submission.answerTooLargeError": { - "defaultMessage": "제출한 답변은 2MB 미만이어야 합니다." - }, "course.assessment.submission.CodaveriFeedbackStatus.codaveriFeedbackStatus": { "defaultMessage": "Codaveri 피드백 상태" }, @@ -3203,7 +3188,28 @@ "course.assessment.submission.FileInput.uploadLabel": { "defaultMessage": "파일을 업로드하려면 드래그 앤 드롭하거나 클릭하세요." }, - "course.assessment.submission.ImportedFileView.deleteConfirmation": { + "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { + "defaultMessage": "{filename}:{lineNumber}" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { + "defaultMessage": "{lineNumber} 줄" + }, + "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { + "defaultMessage": "위의 채팅이 종료되었습니다. 새 채팅을 시작하시겠습니까?" + }, + "course.assessment.submission.GetHelpChatPage.chatInputText": { + "defaultMessage": "어떻게 도와드릴까요?" + }, + "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { + "defaultMessage": "{numMessages} / {maxMessages} 개 메시지 남음" + }, + "course.assessment.submission.GetHelpChatPage.codeUpdated": { + "defaultMessage": "코드가 업데이트되었습니다" + }, + "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { + "defaultMessage": "이 질문의 메시지 한도에 도달했습니다." + }, + "course.assessment.submission.ImportedFileView.deleteConfirmation": { "defaultMessage": "이 파일을 삭제하시겠습니까?" }, "course.assessment.submission.ImportedFileView.noFiles": { @@ -3260,9 +3266,6 @@ "course.assessment.submission.SubmissionsIndex.includePhantoms": { "defaultMessage": "팬텀 사용자 포함" }, - "lib.translations.myStudents": { - "defaultMessage": "내 학생들" - }, "course.assessment.submission.SubmissionsIndex.phantom": { "defaultMessage": "팬텀 사용자" }, @@ -3278,12 +3281,6 @@ "course.assessment.submission.SubmissionsIndex.remind": { "defaultMessage": "알림 이메일 보내기" }, - "lib.translations.staff": { - "defaultMessage": "스태프" - }, - "lib.translations.students": { - "defaultMessage": "학생들" - }, "course.assessment.submission.SubmissionsIndex.submissionStatus": { "defaultMessage": "상태" }, @@ -3473,6 +3470,12 @@ "course.assessment.submission.answerSubmitted": { "defaultMessage": "답변 제출됨" }, + "course.assessment.submission.answerTooLarge": { + "defaultMessage": "답변이 너무 큽니다" + }, + "course.assessment.submission.answerTooLargeError": { + "defaultMessage": "제출한 답변은 2MB 미만이어야 합니다." + }, "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { "defaultMessage": "과거 답변이 없습니다." }, @@ -3587,6 +3590,12 @@ "course.assessment.submission.bonusEndAt": { "defaultMessage": "보너스 종료 시각" }, + "course.assessment.submission.checkAnswer": { + "defaultMessage": "답변 확인" + }, + "course.assessment.submission.checkAnswerWithLimit": { + "defaultMessage": "답변 확인 ({attemptsLeft, plural, one {# 시도} other {# 시도}} 남음)" + }, "course.assessment.submission.codaveriAutogradeFailure": { "defaultMessage": "Codaveri에서 코드를 평가하는 동안 오류가 발생했습니다. 몇 분 후에 코드를 다시 제출하거나 네트워크 응답의 오류 메시지를 확인하세요." }, @@ -3614,14 +3623,14 @@ "course.assessment.submission.comment.CommentCard.deleteConfirmation": { "defaultMessage": "이 댓글을 삭제하시겠습니까?" }, - "course.assessment.submission.comment.CommentCard.save": { - "defaultMessage": "저장" + "course.assessment.submission.comment.CommentCard.isAiGenerated": { + "defaultMessage": "AI 생성 댓글" }, "course.assessment.submission.comment.CommentCard.publish": { "defaultMessage": "게시" }, - "course.assessment.submission.comment.CommentCard.isAiGenerated": { - "defaultMessage": "AI 생성 댓글" + "course.assessment.submission.comment.CommentCard.save": { + "defaultMessage": "저장" }, "course.assessment.submission.comment.CommentField.comment": { "defaultMessage": "댓글" @@ -3860,14 +3869,14 @@ "course.assessment.submission.question": { "defaultMessage": "질문" }, - "course.assessment.submission.questionNumber": { - "defaultMessage": "Q{number}" + "course.assessment.submission.questionAnswer": { + "defaultMessage": "응답" }, "course.assessment.submission.questionDescription": { "defaultMessage": "설명" }, - "course.assessment.submission.questionAnswer": { - "defaultMessage": "응답" + "course.assessment.submission.questionNumber": { + "defaultMessage": "Q{number}" }, "course.assessment.submission.readOnlyEditor.expandComments": { "defaultMessage": "모든 댓글 확장" @@ -3890,15 +3899,6 @@ "course.assessment.submission.resetConfirmation": { "defaultMessage": "답변을 재설정하시겠습니까? 이 조치는 되돌릴 수 없으며 이 질문에 대한 모든 현재 작업이 손실됩니다." }, - "course.assessment.submission.checkAnswer": { - "defaultMessage": "답변 확인" - }, - "course.assessment.submission.checkAnswerWithLimit": { - "defaultMessage": "답변 확인 ({attemptsLeft, plural, one {# 시도} other {# 시도}} 남음)" - }, - "course.assessment.submission.submitWithLimit": { - "defaultMessage": "제출 ({attemptsLeft, plural, one {# 시도} other {# 시도}} 남음)" - }, "course.assessment.submission.saveDraft": { "defaultMessage": "초안 저장" }, @@ -3953,21 +3953,24 @@ "course.assessment.submission.submitShortcut": { "defaultMessage": "(Ctrl+Enter) 또는 (⌘+Enter)" }, + "course.assessment.submission.submitWithLimit": { + "defaultMessage": "제출 ({attemptsLeft, plural, one {# 시도} other {# 시도}} 남음)" + }, "course.assessment.submission.submitted": { "defaultMessage": "제출됨" }, "course.assessment.submission.submittedAt": { "defaultMessage": "제출 시각" }, - "course.assessment.submission.unknown": { - "defaultMessage": "알 수 없는 상태, 관리자에게 문의하세요" - }, "course.assessment.submission.totalGrade": { "defaultMessage": "총 등급" }, "course.assessment.submission.type": { "defaultMessage": "유형" }, + "course.assessment.submission.unknown": { + "defaultMessage": "알 수 없는 상태, 관리자에게 문의하세요" + }, "course.assessment.submission.unmark": { "defaultMessage": "제출로 되돌리기" }, @@ -4163,9 +4166,6 @@ "course.asssessment.submission.submitNoQuestionExplain": { "defaultMessage": "완료로 표시하시겠습니까?" }, - "course.admin.NotificationSettings.component": { - "defaultMessage": "구성 요소" - }, "course.componentTitles.course_achievements_component": { "defaultMessage": "성과" }, @@ -4250,15 +4250,6 @@ "course.courses.CourseAnnouncements.announcementHeader": { "defaultMessage": "최근 공지 사항" }, - "course.courses.CourseSuspendedAlert.header": { - "defaultMessage": "이 과정은 현재 정지되어 있습니다. 강사는 계속 접근할 수 있지만 학생은 접근할 수 없습니다." - }, - "course.courses.CourseSuspendedAlert.canSuspendMessage": { - "defaultMessage": "{link} 페이지에서 정지를 해제할 수 있습니다." - }, - "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { - "defaultMessage": "이것이 실수라고 생각되면, 과정 관리자 또는 소유자에게 연락하여 정지를 해제해 달라고 요청하세요." - }, "course.courses.CourseDisplay.noCourse": { "defaultMessage": "아직 과정이 없습니다..." }, @@ -4304,6 +4295,15 @@ "course.courses.CourseShow.instructorsHeader": { "defaultMessage": "교사" }, + "course.courses.CourseSuspendedAlert.canSuspendMessage": { + "defaultMessage": "{link} 페이지에서 정지를 해제할 수 있습니다." + }, + "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { + "defaultMessage": "이것이 실수라고 생각되면, 과정 관리자 또는 소유자에게 연락하여 정지를 해제해 달라고 요청하세요." + }, + "course.courses.CourseSuspendedAlert.header": { + "defaultMessage": "이 과정은 현재 정지되어 있습니다. 강사는 계속 접근할 수 있지만 학생은 접근할 수 없습니다." + }, "course.courses.CourseUserItem.differentCourseNameHint": { "defaultMessage": "이 과정의 관리자가 이 이름으로 초대했기 때문에 계정 이름과 다른 이름을 보고 계십니다." }, @@ -4472,15 +4472,15 @@ "course.courses.SidebarItem.home": { "defaultMessage": "홈" }, + "course.courses.SidebarItem.scholaistic.assessments": { + "defaultMessage": "롤플레잉 평가" + }, "course.courses.SidebarItem.stories.learn": { "defaultMessage": "배우기" }, "course.courses.SidebarItem.stories.missionControl": { "defaultMessage": "미션 컨트롤" }, - "course.courses.SidebarItem.scholaistic.assessments": { - "defaultMessage": "롤플레잉 평가" - }, "course.courses.TodoIgnoreButton.ignore.ignoreButtonText": { "defaultMessage": "무시" }, @@ -4523,6 +4523,12 @@ "course.discussion.topics.CommentCard.deleteSuccess": { "defaultMessage": "댓글이 성공적으로 삭제되었습니다." }, + "course.discussion.topics.CommentCard.isAiGenerated": { + "defaultMessage": "AI 생성 초안 댓글" + }, + "course.discussion.topics.CommentCard.publish": { + "defaultMessage": "게시" + }, "course.discussion.topics.CommentCard.publishFailure": { "defaultMessage": "피드백 게시 실패." }, @@ -4544,12 +4550,6 @@ "course.discussion.topics.CommentCard.updateSuccess": { "defaultMessage": "댓글이 성공적으로 업데이트되었습니다." }, - "course.discussion.topics.CommentCard.publish": { - "defaultMessage": "게시" - }, - "course.discussion.topics.CommentCard.isAiGenerated": { - "defaultMessage": "AI 생성 초안 댓글" - }, "course.discussion.topics.CommentField.comment": { "defaultMessage": "댓글" }, @@ -4823,21 +4823,6 @@ "course.enrolRequests.UserRequests.rejected": { "defaultMessage": "거부된 등록 요청" }, - "course.experiencePoints.downloadCsvButton": { - "defaultMessage": "CSV 다운로드" - }, - "course.experiencePoints.downloadFailure": { - "defaultMessage": "다운로드 요청을 처리하는 중 오류가 발생했습니다." - }, - "course.experiencePoints.downloadPending": { - "defaultMessage": "다운로드 요청을 처리하는 동안 기다려 주세요." - }, - "course.experiencePoints.downloadRequestSuccess": { - "defaultMessage": "다운로드 요청이 성공했습니다" - }, - "course.experiencePoints.filterByNameButton": { - "defaultMessage": "이름으로 필터링" - }, "course.experiencePoints.disbursement.DisbursementForm.createDisbursementFailure": { "defaultMessage": "경험치 부여 실패." }, @@ -4958,6 +4943,21 @@ "course.experiencePoints.disbursement.ForumPostTable.voteTally": { "defaultMessage": "투표 집계" }, + "course.experiencePoints.downloadCsvButton": { + "defaultMessage": "CSV 다운로드" + }, + "course.experiencePoints.downloadFailure": { + "defaultMessage": "다운로드 요청을 처리하는 중 오류가 발생했습니다." + }, + "course.experiencePoints.downloadPending": { + "defaultMessage": "다운로드 요청을 처리하는 동안 기다려 주세요." + }, + "course.experiencePoints.downloadRequestSuccess": { + "defaultMessage": "다운로드 요청이 성공했습니다" + }, + "course.experiencePoints.filterByNameButton": { + "defaultMessage": "이름으로 필터링" + }, "course.forum.FormShow.fetchTopicsFailure": { "defaultMessage": "포럼 주제 데이터를 검색하는 데 실패했습니다." }, @@ -5255,155 +5255,503 @@ "course.forum.MarkAnswerButton.markAsAnswer": { "defaultMessage": "답변으로 표시" }, - "course.forum.MarkAnswerButton.markedAsAnswer": { - "defaultMessage": "답변으로 표시됨" + "course.forum.MarkAnswerButton.markedAsAnswer": { + "defaultMessage": "답변으로 표시됨" + }, + "course.forum.MarkAnswerButton.unmarkAsAnswer": { + "defaultMessage": "답변으로 표시 취소" + }, + "course.forum.MarkAnswerButton.updateFailure": { + "defaultMessage": "게시물 업데이트 실패 - {error}" + }, + "course.forum.NextUnreadButton.AllReadTooltip": { + "defaultMessage": "만세! 모든 주제가 읽혔습니다!" + }, + "course.forum.NextUnreadButton.nextUnread": { + "defaultMessage": "다음 읽지 않은 것" + }, + "course.forum.NextUnreadButton.nextUnreadTooltip": { + "defaultMessage": "다음 읽지 않은 주제로 이동" + }, + "course.forum.PostCreatorObject.anonymousUser": { + "defaultMessage": "익명 사용자" + }, + "course.forum.PostCreatorObject.maskUser": { + "defaultMessage": "사용자 가리기" + }, + "course.forum.PostCreatorObject.postAnonymously": { + "defaultMessage": "익명 게시" + }, + "course.forum.PostCreatorObject.unmaskUser": { + "defaultMessage": "사용자 가리기 해제" + }, + "course.forum.ReplyCard.emptyPost": { + "defaultMessage": "게시물을 비울 수 없습니다!" + }, + "course.forum.ReplyCard.postAnonymously": { + "defaultMessage": "익명 게시" + }, + "course.forum.ReplyCard.replyFailure": { + "defaultMessage": "게시물 제출 실패 - {error}" + }, + "course.forum.ReplyCard.replySuccess": { + "defaultMessage": "답글 게시물이 생성되었습니다." + }, + "course.forum.ReplyCard.replyTo": { + "defaultMessage": "{user}에게 답글" + }, + "course.forum.SubscribeButton.commonTranslations.manageMySubscription": { + "defaultMessage": "내 구독 관리" + }, + "course.forum.SubscribeButton.commonTranslations.subscribe": { + "defaultMessage": "구독" + }, + "course.forum.SubscribeButton.commonTranslations.unsubscribe": { + "defaultMessage": "구독 취소" + }, + "course.forum.SubscribeButton.commonTranslations.updateSubscriptionFailure": { + "defaultMessage": "구독 업데이트 실패 - {error}" + }, + "course.forum.SubscribeButton.forumTopicTranslations.adminSettingSubscribed": { + "defaultMessage": "포럼 주제 구독이 강좌 관리자에 의해 비활성화되었습니다." + }, + "course.forum.SubscribeButton.forumTopicTranslations.subscribeSuccess": { + "defaultMessage": "포럼 주제 {title}에 성공적으로 구독되었습니다." + }, + "course.forum.SubscribeButton.forumTopicTranslations.subscribeTooltip": { + "defaultMessage": "누군가가 이 포럼 주제에서 답변할 때 이메일 알림을 받으려면 구독하세요." + }, + "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeSuccess": { + "defaultMessage": "포럼 주제 {title}에서 성공적으로 구독 취소되었습니다." + }, + "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeTooltip": { + "defaultMessage": "누군가가 이 포럼 주제에서 답변할 때 이메일 알림을 받는 것을 중지하려면 구독 취소하세요." + }, + "course.forum.SubscribeButton.forumTopicTranslations.userSettingSubscribed": { + "defaultMessage": "이 과정의 포럼에서 \"새 게시물 및 답변\" 구독을 취소했습니다. 활성화하려면 {manageMySubscriptionLink}로 이동하세요." + }, + "course.forum.SubscribeButton.forumTranslations.adminSettingSubscribed": { + "defaultMessage": "새 포럼 주제의 구독이 강좌 관리자에 의해 비활성화되었습니다." + }, + "course.forum.SubscribeButton.forumTranslations.subscribeSuccess": { + "defaultMessage": "{title}에 성공적으로 구독되었습니다." + }, + "course.forum.SubscribeButton.forumTranslations.subscribeTooltip": { + "defaultMessage": "새 주제가 생성될 때 이메일 알림을 받으려면 구독하세요." + }, + "course.forum.SubscribeButton.forumTranslations.unsubscribeSuccess": { + "defaultMessage": "{title}에서 성공적으로 구독 취소되었습니다." + }, + "course.forum.SubscribeButton.forumTranslations.unsubscribeTooltip": { + "defaultMessage": "새 주제가 생성될 때 이메일 알림을 받는 것을 중지하려면 구독 취소하세요." + }, + "course.forum.SubscribeButton.forumTranslations.userSettingSubscribed": { + "defaultMessage": "이 과정의 포럼에서 \"새 주제\" 구독을 취소했습니다. 활성화하려면 {manageMySubscriptionLink}로 이동하세요." + }, + "course.forum.VotePostButton.updateFailure": { + "defaultMessage": "투표 수 업데이트 실패 - {error}" + }, + "course.forum.forum.markAllAsReadFailed": { + "defaultMessage": "이 포럼의 모든 주제를 읽음으로 표시하는 데 실패했습니다. 나중에 다시 시도하세요." + }, + "course.gradebook.AddExternalColumnPrompt.cancel": { + "defaultMessage": "취소" + }, + "course.gradebook.AddExternalColumnPrompt.create": { + "defaultMessage": "만들기" + }, + "course.gradebook.AddExternalColumnPrompt.error": { + "defaultMessage": "외부 평가를 생성할 수 없습니다." + }, + "course.gradebook.AddExternalColumnPrompt.maxLabel": { + "defaultMessage": "최대 점수" + }, + "course.gradebook.AddExternalColumnPrompt.nameLabel": { + "defaultMessage": "이름" + }, + "course.gradebook.AddExternalColumnPrompt.success": { + "defaultMessage": "외부 평가가 생성되었습니다." + }, + "course.gradebook.AddExternalColumnPrompt.title": { + "defaultMessage": "외부 평가 추가" + }, + "course.gradebook.ConfigureWeightsPrompt.allExcluded": { + "defaultMessage": "“{tab}”의 모든 평가가 제외되어 총점에 반영되지 않습니다." + }, + "course.gradebook.ConfigureWeightsPrompt.allExcludedCount": { + "defaultMessage": "전체 {n}개 제외됨" + }, + "course.gradebook.ConfigureWeightsPrompt.customMode": { + "defaultMessage": "사용자 지정" + }, + "course.gradebook.ConfigureWeightsPrompt.customSum": { + "defaultMessage": "평가 가중치: {sum} / {total}" + }, + "course.gradebook.ConfigureWeightsPrompt.defaultsHint": { + "defaultMessage": "아직 설정된 가중치가 없습니다. 모든 탭이 동일한 비중으로 반영되도록 제안된 기본값입니다. 저장하여 확정하거나 아래에서 조정하세요." + }, + "course.gradebook.ConfigureWeightsPrompt.descriptionDrop": { + "defaultMessage": "동일 모드에서는 평균을 내기 전에 각 학생의 점수가 가장 낮은 평가 N개를 선택적으로 제외할 수 있습니다." + }, + "course.gradebook.ConfigureWeightsPrompt.descriptionExclusion": { + "defaultMessage": "탭을 펼쳐 개별 평가를 성적에 포함하거나 제외하세요." + }, + "course.gradebook.ConfigureWeightsPrompt.descriptionIntro": { + "defaultMessage": "각 탭과 평가가 학생의 총 성적에 어떻게 반영되는지 관리합니다." + }, + "course.gradebook.ConfigureWeightsPrompt.descriptionModes": { + "defaultMessage": "“동일”(모든 평가가 탭의 가중치를 동일하게 나눔)또는 “사용자 지정”(각 평가의 비중을 설정)을 선택하세요." + }, + "course.gradebook.ConfigureWeightsPrompt.descriptionWeights": { + "defaultMessage": "각 탭의 가중치, 즉 총 성적에 기여하는 비중을 설정하세요. 가중치 합계는 100이어야 합니다." + }, + "course.gradebook.ConfigureWeightsPrompt.equalMode": { + "defaultMessage": "동일" + }, + "course.gradebook.ConfigureWeightsPrompt.excluded": { + "defaultMessage": "제외됨" + }, + "course.gradebook.ConfigureWeightsPrompt.excludedCount": { + "defaultMessage": "{n}개 제외됨" + }, + "course.gradebook.ConfigureWeightsPrompt.includeAssessment": { + "defaultMessage": "{assessment} 성적에 포함" + }, + "course.gradebook.ConfigureWeightsPrompt.modeAria": { + "defaultMessage": "{tab} 가중치 모드" + }, + "course.gradebook.ConfigureWeightsPrompt.ofGrade": { + "defaultMessage": "성적의 {pct}%" + }, + "course.gradebook.ConfigureWeightsPrompt.promptTitle": { + "defaultMessage": "기여도 설정" + }, + "course.gradebook.ConfigureWeightsPrompt.saveError": { + "defaultMessage": "가중치를 저장하지 못했습니다. 다시 시도해 주세요." + }, + "course.gradebook.ConfigureWeightsPrompt.total": { + "defaultMessage": "합계: {sum}%" + }, + "course.gradebook.ConfigureWeightsPrompt.unbalanced": { + "defaultMessage": "저장하기 전에 “{tab}”의 평가 가중치 합계가 해당 탭의 총 가중치와 같아야 합니다." + }, + "course.gradebook.ConfigureWeightsPrompt.valueTooHigh": { + "defaultMessage": "값은 최대 100이어야 합니다" + }, + "course.gradebook.ConfigureWeightsPrompt.valueTooLow": { + "defaultMessage": "값은 최소 0이어야 합니다" + }, + "course.gradebook.ConfigureWeightsPrompt.weightsDoNotSum": { + "defaultMessage": "가중치 합계가 100이 아닙니다. 저장은 가능하지만 총점이 정확하지 않을 수 있습니다." + }, + "course.gradebook.DeleteExternalColumnPrompt.body": { + "defaultMessage": "\"{title}\"을(를) 삭제하시겠습니까? 이 작업은 해당 열과 그 안의 모든 학생 성적을 영구적으로 제거합니다. 이 작업은 되돌릴 수 없습니다." + }, + "course.gradebook.DeleteExternalColumnPrompt.cancel": { + "defaultMessage": "취소" + }, + "course.gradebook.DeleteExternalColumnPrompt.confirm": { + "defaultMessage": "삭제" + }, + "course.gradebook.DeleteExternalColumnPrompt.error": { + "defaultMessage": "외부 평가를 삭제할 수 없습니다." + }, + "course.gradebook.DeleteExternalColumnPrompt.title": { + "defaultMessage": "외부 평가 삭제" + }, + "course.gradebook.ExternalGradeConflictPrompt.body": { + "defaultMessage": "이 학생들은 이미 해당 구성 요소에 대한 성적이 있습니다. 기존 성적을 유지하시겠습니까, 아니면 파일의 값으로 대체하시겠습니까? 새 학생과 빈 셀은 영향을 받지 않습니다." + }, + "course.gradebook.ExternalGradeConflictPrompt.goBack": { + "defaultMessage": "돌아가기" + }, + "course.gradebook.ExternalGradeConflictPrompt.keepExisting": { + "defaultMessage": "기존 항목 유지" + }, + "course.gradebook.ExternalGradeConflictPrompt.replace": { + "defaultMessage": "대체" + }, + "course.gradebook.ExternalGradeConflictPrompt.title": { + "defaultMessage": "성적 충돌 해결" + }, + "course.gradebook.ExternalGradeConflictTable.component": { + "defaultMessage": "구성 요소" + }, + "course.gradebook.ExternalGradeConflictTable.existing": { + "defaultMessage": "기존 성적" + }, + "course.gradebook.ExternalGradeConflictTable.inFile": { + "defaultMessage": "파일 내 성적" + }, + "course.gradebook.ExternalGradeConflictTable.mismatch": { + "defaultMessage": "이 식별자는 이제 기존 성적을 가져올 때의 학생과 다른 학생으로 확인됩니다." + }, + "course.gradebook.ExternalGradeConflictTable.student": { + "defaultMessage": "학생" + }, + "course.gradebook.GradeLinkHint.hint": { + "defaultMessage": "각 성적은 학생 제출물의 점수 합계입니다. 성적을 클릭하면 해당 제출물을 열고 점수를 조정할 수 있습니다." + }, + "course.gradebook.GradebookColumnTree.alwaysIncluded": { + "defaultMessage": "필수 항목" + }, + "course.gradebook.GradebookColumnTree.gamification": { + "defaultMessage": "게임화 관련" + }, + "course.gradebook.GradebookColumnTree.grades": { + "defaultMessage": "성적" + }, + "course.gradebook.GradebookColumnTree.studentInfo": { + "defaultMessage": "학생 정보" + }, + "course.gradebook.GradebookIndex.addExternal": { + "defaultMessage": "외부 평가 추가" + }, + "course.gradebook.GradebookIndex.allAssessments": { + "defaultMessage": "전체 평가" + }, + "course.gradebook.GradebookIndex.applyAndExport": { + "defaultMessage": "적용 및 내보내기" + }, + "course.gradebook.GradebookIndex.byWeight": { + "defaultMessage": "가중 총점" + }, + "course.gradebook.GradebookIndex.dialogTitle": { + "defaultMessage": "열 선택" + }, + "course.gradebook.GradebookIndex.exportAllTooltip": { + "defaultMessage": "선택한 행 없음 - 모든 행이 내보내집니다." + }, + "course.gradebook.GradebookIndex.exportButton": { + "defaultMessage": "전체 행 내보내기" + }, + "course.gradebook.GradebookIndex.exportRows": { + "defaultMessage": "{count, plural, other {# 행}} 내보내기" + }, + "course.gradebook.GradebookIndex.fetchFailure": { + "defaultMessage": "성적부를 불러오지 못했습니다." + }, + "course.gradebook.GradebookIndex.gradebook": { + "defaultMessage": "성적부" + }, + "course.gradebook.GradebookIndex.noStudents": { + "defaultMessage": "학생 없음" + }, + "course.gradebook.GradebookIndex.noStudentsHint": { + "defaultMessage": "학생이 강좌에 참여하면 여기에 성적이 표시됩니다." + }, + "course.gradebook.GradebookIndex.searchStudents": { + "defaultMessage": "학생 검색" + }, + "course.gradebook.GradebookIndex.selectColumns": { + "defaultMessage": "열 선택" + }, + "course.gradebook.GradebookTable.delete": { + "defaultMessage": "삭제" + }, + "course.gradebook.GradebookTable.externalBadge": { + "defaultMessage": "외부" + }, + "course.gradebook.GradebookTable.externalGradeAria": { + "defaultMessage": "{name}의 {title} 성적" + }, + "course.gradebook.GradebookTable.externalMaxAria": { + "defaultMessage": "{title} 최대 점수" + }, + "course.gradebook.GradebookTable.gradeSaveError": { + "defaultMessage": "성적을 저장할 수 없습니다. 다시 시도해 주세요." + }, + "course.gradebook.GradebookTable.manageAria": { + "defaultMessage": "{title} 관리" + }, + "course.gradebook.GradebookTable.maxMarks": { + "defaultMessage": "최고 점수" + }, + "course.gradebook.GradebookTable.maxSaveError": { + "defaultMessage": "최대 점수를 저장할 수 없습니다. 다시 시도해 주세요." + }, + "course.gradebook.GradebookTable.noDataColumnsHint": { + "defaultMessage": "성적 열을 선택하지 않았습니다. 내보내기에는 학생 정보만 포함됩니다." + }, + "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { + "defaultMessage": "성적 또는 게임화 열을 선택하지 않았습니다. 내보내기에는 학생 정보만 포함됩니다." + }, + "course.gradebook.GradebookTable.rename": { + "defaultMessage": "이름 변경" + }, + "course.gradebook.GradebookWeightedTable.collapseRow": { + "defaultMessage": "{name} 접기" + }, + "course.gradebook.GradebookWeightedTable.configureWeights": { + "defaultMessage": "가중치 설정" + }, + "course.gradebook.GradebookWeightedTable.defaultWeights": { + "defaultMessage": "기본 가중치를 표시하고 있습니다. 모든 탭이 동일하게 반영됩니다. \"가중치 설정\"을 클릭하여 직접 설정하세요." + }, + "course.gradebook.GradebookWeightedTable.defaultWeightsNoAccess": { + "defaultMessage": "기본 가중치를 표시하고 있습니다. 가중치가 설정되기 전까지 모든 탭이 동일하게 반영됩니다." + }, + "course.gradebook.GradebookWeightedTable.displayMode": { + "defaultMessage": "표시 모드" + }, + "course.gradebook.GradebookWeightedTable.displayPercent": { + "defaultMessage": "백분율" + }, + "course.gradebook.GradebookWeightedTable.displayPercentTooltip": { + "defaultMessage": "학생이 각 탭에서 획득한 비율입니다. 비중이 20%인 탭에서 100%는 해당 탭의 20점을 모두 획득했음을 의미합니다." + }, + "course.gradebook.GradebookWeightedTable.displayPoints": { + "defaultMessage": "점수" + }, + "course.gradebook.GradebookWeightedTable.displayPointsTooltip": { + "defaultMessage": "각 탭이 기여하는 성적 점수입니다. 각 열의 합이 예상 총점이 됩니다." }, - "course.forum.MarkAnswerButton.unmarkAsAnswer": { - "defaultMessage": "답변으로 표시 취소" + "course.gradebook.GradebookWeightedTable.downloadCsv": { + "defaultMessage": "CSV로 다운로드" }, - "course.forum.MarkAnswerButton.updateFailure": { - "defaultMessage": "게시물 업데이트 실패 - {error}" + "course.gradebook.GradebookWeightedTable.email": { + "defaultMessage": "이메일" }, - "course.forum.NextUnreadButton.AllReadTooltip": { - "defaultMessage": "만세! 모든 주제가 읽혔습니다!" + "course.gradebook.GradebookWeightedTable.excluded": { + "defaultMessage": "제외됨" }, - "course.forum.NextUnreadButton.nextUnread": { - "defaultMessage": "다음 읽지 않은 것" + "course.gradebook.GradebookWeightedTable.expandRow": { + "defaultMessage": "{name} 펼치기" }, - "course.forum.NextUnreadButton.nextUnreadTooltip": { - "defaultMessage": "다음 읽지 않은 주제로 이동" + "course.gradebook.GradebookWeightedTable.name": { + "defaultMessage": "이름" }, - "course.forum.PostCreatorObject.anonymousUser": { - "defaultMessage": "익명 사용자" + "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { + "defaultMessage": "설정된 가중치가 없습니다. 모든 탭의 가중치가 0입니다. \"가중치 설정\"을 클릭하여 가중치를 지정하세요." }, - "course.forum.PostCreatorObject.maskUser": { - "defaultMessage": "사용자 가리기" + "course.gradebook.GradebookWeightedTable.noWeightsNoAccess": { + "defaultMessage": "아직 탭 가중치가 설정되지 않았습니다." }, - "course.forum.PostCreatorObject.postAnonymously": { - "defaultMessage": "익명 게시" + "course.gradebook.GradebookWeightedTable.outOfWeight": { + "defaultMessage": "/{weight}" }, - "course.forum.PostCreatorObject.unmaskUser": { - "defaultMessage": "사용자 가리기 해제" + "course.gradebook.GradebookWeightedTable.percentOfGrade": { + "defaultMessage": "성적의 {weight}%" }, - "course.forum.ReplyCard.emptyPost": { - "defaultMessage": "게시물을 비울 수 없습니다!" + "course.gradebook.GradebookWeightedTable.percentTotalExact": { + "defaultMessage": "합계 100%" }, - "course.forum.ReplyCard.postAnonymously": { - "defaultMessage": "익명 게시" + "course.gradebook.GradebookWeightedTable.percentTotalWarning": { + "defaultMessage": "합계 {weight}%" }, - "course.forum.ReplyCard.replyFailure": { - "defaultMessage": "게시물 제출 실패 - {error}" + "course.gradebook.GradebookWeightedTable.searchStudents": { + "defaultMessage": "학생 검색" }, - "course.forum.ReplyCard.replySuccess": { - "defaultMessage": "답글 게시물이 생성되었습니다." + "course.gradebook.GradebookWeightedTable.total": { + "defaultMessage": "총점" }, - "course.forum.ReplyCard.replyTo": { - "defaultMessage": "{user}에게 답글" + "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { + "defaultMessage": "가중치 합계가 100이 아닙니다. 총점이 정확하지 않을 수 있습니다." }, - "course.forum.SubscribeButton.commonTranslations.manageMySubscription": { - "defaultMessage": "내 구독 관리" + "course.gradebook.ImportExternalAssessmentsButton.label": { + "defaultMessage": "외부 평가 가져오기" }, - "course.forum.SubscribeButton.commonTranslations.subscribe": { - "defaultMessage": "구독" + "course.gradebook.ImportWizard.addComponent": { + "defaultMessage": "구성 요소 추가" }, - "course.forum.SubscribeButton.commonTranslations.unsubscribe": { - "defaultMessage": "구독 취소" + "course.gradebook.ImportWizard.back": { + "defaultMessage": "뒤로" }, - "course.forum.SubscribeButton.commonTranslations.updateSubscriptionFailure": { - "defaultMessage": "구독 업데이트 실패 - {error}" + "course.gradebook.ImportWizard.cancel": { + "defaultMessage": "취소" }, - "course.forum.SubscribeButton.forumTopicTranslations.adminSettingSubscribed": { - "defaultMessage": "포럼 주제 구독이 강좌 관리자에 의해 비활성화되었습니다." + "course.gradebook.ImportWizard.commitError": { + "defaultMessage": "가져오기에 실패했습니다. 아무것도 저장되지 않았습니다." }, - "course.forum.SubscribeButton.forumTopicTranslations.subscribeSuccess": { - "defaultMessage": "포럼 주제 {title}에 성공적으로 구독되었습니다." + "course.gradebook.ImportWizard.committed": { + "defaultMessage": "가져오기가 완료되었습니다." }, - "course.forum.SubscribeButton.forumTopicTranslations.subscribeTooltip": { - "defaultMessage": "누군가가 이 포럼 주제에서 답변할 때 이메일 알림을 받으려면 구독하세요." + "course.gradebook.ImportWizard.componentName": { + "defaultMessage": "구성 요소 이름" }, - "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeSuccess": { - "defaultMessage": "포럼 주제 {title}에서 성공적으로 구독 취소되었습니다." + "course.gradebook.ImportWizard.continue": { + "defaultMessage": "가져오기 확인" }, - "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeTooltip": { - "defaultMessage": "누군가가 이 포럼 주제에서 답변할 때 이메일 알림을 받는 것을 중지하려면 구독 취소하세요." + "course.gradebook.ImportWizard.downloadTemplate": { + "defaultMessage": "템플릿 다운로드" }, - "course.forum.SubscribeButton.forumTopicTranslations.userSettingSubscribed": { - "defaultMessage": "이 과정의 포럼에서 \"새 게시물 및 답변\" 구독을 취소했습니다. 활성화하려면 {manageMySubscriptionLink}로 이동하세요." + "course.gradebook.ImportWizard.email": { + "defaultMessage": "이메일" }, - "course.forum.SubscribeButton.forumTranslations.adminSettingSubscribed": { - "defaultMessage": "새 포럼 주제의 구독이 강좌 관리자에 의해 비활성화되었습니다." + "course.gradebook.ImportWizard.identifierMode": { + "defaultMessage": "학생 매칭 기준" }, - "course.forum.SubscribeButton.forumTranslations.subscribeSuccess": { - "defaultMessage": "{title}에 성공적으로 구독되었습니다." + "course.gradebook.ImportWizard.malformed": { + "defaultMessage": "다음 셀은 올바른 숫자가 아닙니다: {cells}" }, - "course.forum.SubscribeButton.forumTranslations.subscribeTooltip": { - "defaultMessage": "새 주제가 생성될 때 이메일 알림을 받으려면 구독하세요." + "course.gradebook.ImportWizard.maxMarks": { + "defaultMessage": "최대 점수" }, - "course.forum.SubscribeButton.forumTranslations.unsubscribeSuccess": { - "defaultMessage": "{title}에서 성공적으로 구독 취소되었습니다." + "course.gradebook.ImportWizard.next": { + "defaultMessage": "다음" }, - "course.forum.SubscribeButton.forumTranslations.unsubscribeTooltip": { - "defaultMessage": "새 주제가 생성될 때 이메일 알림을 받는 것을 중지하려면 구독 취소하세요." + "course.gradebook.ImportWizard.previewError": { + "defaultMessage": "파일을 확인할 수 없습니다. 다시 시도해 주세요." }, - "course.forum.SubscribeButton.forumTranslations.userSettingSubscribed": { - "defaultMessage": "이 과정의 포럼에서 \"새 주제\" 구독을 취소했습니다. 활성화하려면 {manageMySubscriptionLink}로 이동하세요." + "course.gradebook.ImportWizard.stepDefine": { + "defaultMessage": "구성 요소 정의" }, - "course.forum.VotePostButton.updateFailure": { - "defaultMessage": "투표 수 업데이트 실패 - {error}" + "course.gradebook.ImportWizard.stepUpload": { + "defaultMessage": "템플릿 및 업로드" }, - "course.forum.forum.markAllAsReadFailed": { - "defaultMessage": "이 포럼의 모든 주제를 읽음으로 표시하는 데 실패했습니다. 나중에 다시 시도하세요." + "course.gradebook.ImportWizard.stepVerify": { + "defaultMessage": "확인" }, - "course.gradebook.GradebookColumnTree.grades": { - "defaultMessage": "성적" + "course.gradebook.ImportWizard.studentId": { + "defaultMessage": "학생 ID" }, - "course.gradebook.GradebookColumnTree.studentInfo": { - "defaultMessage": "학생 정보" + "course.gradebook.ImportWizard.studentIdHint": { + "defaultMessage": "매칭에는 각 학생의 현재 학생 ID가 사용됩니다. 사용자 관리에서 학생 ID를 최신 상태로 유지하세요." }, - "course.gradebook.GradebookColumnTree.alwaysIncluded": { - "defaultMessage": "필수 항목" + "course.gradebook.ImportWizard.title": { + "defaultMessage": "외부 평가 가져오기" }, - "course.gradebook.GradebookColumnTree.gamification": { - "defaultMessage": "게임화 관련" + "course.gradebook.ImportWizard.unresolved": { + "defaultMessage": "다음 식별자를 강좌에서 찾을 수 없습니다: {ids}" }, - "course.gradebook.GradebookIndex.dialogTitle": { - "defaultMessage": "열 선택" + "course.gradebook.ImportWizard.updatesExisting": { + "defaultMessage": "기존 항목 업데이트 - 성적부에서 관리됨" }, - "course.gradebook.GradebookIndex.exportAllTooltip": { - "defaultMessage": "선택한 행 없음 - 모든 행이 내보내집니다." + "course.gradebook.ImportWizard.upload": { + "defaultMessage": "작성한 CSV 업로드" }, - "course.gradebook.GradebookIndex.exportButton": { - "defaultMessage": "전체 행 내보내기" + "course.gradebook.ImportWizard.verify": { + "defaultMessage": "확인" }, - "course.gradebook.GradebookIndex.exportRows": { - "defaultMessage": "{count, plural, other {# 행}} 내보내기" + "course.gradebook.ImportWizard.weightage": { + "defaultMessage": "가중치" }, - "course.gradebook.GradebookIndex.selectColumns": { - "defaultMessage": "열 선택" + "course.gradebook.ProjectedTotalHint.policy": { + "defaultMessage": "총점은 채점되지 않은 평가를 0점으로 계산합니다." }, - "course.gradebook.GradebookIndex.applyAndExport": { - "defaultMessage": "적용 및 내보내기" + "course.gradebook.RenameExternalColumnPrompt.cancel": { + "defaultMessage": "취소" }, - "course.gradebook.GradebookIndex.fetchFailure": { - "defaultMessage": "성적부를 불러오지 못했습니다." + "course.gradebook.RenameExternalColumnPrompt.error": { + "defaultMessage": "외부 평가의 이름을 변경할 수 없습니다." }, - "course.gradebook.GradebookIndex.gradebook": { - "defaultMessage": "성적부" + "course.gradebook.RenameExternalColumnPrompt.nameLabel": { + "defaultMessage": "이름" }, - "course.gradebook.GradebookIndex.searchStudents": { - "defaultMessage": "학생 검색" + "course.gradebook.RenameExternalColumnPrompt.save": { + "defaultMessage": "저장" }, - "course.gradebook.GradebookIndex.noStudents": { - "defaultMessage": "학생 없음" + "course.gradebook.RenameExternalColumnPrompt.title": { + "defaultMessage": "외부 평가 이름 변경" }, - "course.gradebook.GradebookTable.maxMarks": { - "defaultMessage": "최고 점수" + "course.gradebook.TotalHint.policy": { + "defaultMessage": "총점은 채점되지 않은 평가를 0점으로 계산합니다." }, - "course.gradebook.GradebookTable.noDataColumnsHint": { - "defaultMessage": "성적 열을 선택하지 않았습니다. 내보내기에는 학생 정보만 포함됩니다." + "course.gradebook.WeightedViewHint.hint": { + "defaultMessage": "가중 총점이 필요하신가요? 각 탭이 학생의 전체 성적에 반영되는 비중을 설정하고 여기에서 가중 총점을 확인할 수 있습니다. {link}에서 활성화하세요." }, - "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { - "defaultMessage": "성적 또는 게임화 열을 선택하지 않았습니다. 내보내기에는 학생 정보만 포함됩니다." + "course.gradebook.WeightedViewHint.settingsLink": { + "defaultMessage": "성적부 설정" }, "course.group.GroupCreationForm.description": { "defaultMessage": "설명 (선택사항)" @@ -5681,27 +6029,27 @@ "course.leaderboard.LeaderboardTable.average": { "defaultMessage": "평균" }, - "course.leaderboard.LeaderboardTable.experience": { - "defaultMessage": "경험" + "course.leaderboard.LeaderboardTable.averageAchievements": { + "defaultMessage": "평균 성과" }, - "course.leaderboard.LeaderboardTable.rank": { - "defaultMessage": "순위" + "course.leaderboard.LeaderboardTable.averageExperience": { + "defaultMessage": "평균 경험치" }, - "course.leaderboard.LeaderboardTable.name": { - "defaultMessage": "이름" + "course.leaderboard.LeaderboardTable.experience": { + "defaultMessage": "경험" }, "course.leaderboard.LeaderboardTable.level": { "defaultMessage": "레벨" }, - "course.leaderboard.LeaderboardTable.averageExperience": { - "defaultMessage": "평균 경험치" - }, - "course.leaderboard.LeaderboardTable.averageAchievements": { - "defaultMessage": "평균 성과" - }, "course.leaderboard.LeaderboardTable.members": { "defaultMessage": "회원" }, + "course.leaderboard.LeaderboardTable.name": { + "defaultMessage": "이름" + }, + "course.leaderboard.LeaderboardTable.rank": { + "defaultMessage": "순위" + }, "course.leaderboard.LeaderboardTable.titleAchievements": { "defaultMessage": "성과별" }, @@ -5978,6 +6326,15 @@ "course.material.folders.UploadFilesButton.uploadFilesTooltip": { "defaultMessage": "업로드" }, + "course.material.folders.WorkbinTable.lastModified": { + "defaultMessage": "마지막 수정" + }, + "course.material.folders.WorkbinTable.name": { + "defaultMessage": "이름" + }, + "course.material.folders.WorkbinTable.startAt": { + "defaultMessage": "시작 시간" + }, "course.material.folders.WorkbinTableButtons.DeletionFailure": { "defaultMessage": "삭제할 수 없습니다" }, @@ -5990,86 +6347,143 @@ "course.material.folders.WorkbinTableButtons.tableButtonDeleteTooltip": { "defaultMessage": "삭제" }, - "course.material.folders.WorkbinTable.name": { - "defaultMessage": "이름" + "course.plagiarism.PlagiarismIndex.assessments.actions": { + "defaultMessage": "작업" }, - "course.material.folders.WorkbinTable.lastModified": { - "defaultMessage": "마지막 수정" + "course.plagiarism.PlagiarismIndex.assessments.assessment": { + "defaultMessage": "평가" }, - "course.material.folders.WorkbinTable.startAt": { - "defaultMessage": "시작 시간" + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { + "defaultMessage": "선택한 평가 중 일부는 이미 완료된 표절 검사가 있습니다. 새 표절 검사를 실행하면 이전 결과가 제거됩니다." }, - "course.plagiarism.PlagiarismIndex.header.plagiarism": { - "defaultMessage": "표절 검사" + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { + "defaultMessage": "표절 검사를 확인하시겠습니까?" }, - "course.plagiarism.PlagiarismIndex.assessments.assessment": { - "defaultMessage": "평가" + "course.plagiarism.PlagiarismIndex.assessments.lastRunStatus": { + "defaultMessage": "상태" + }, + "course.plagiarism.PlagiarismIndex.assessments.lastRunTime": { + "defaultMessage": "마지막 실행 시각" + }, + "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { + "defaultMessage": "마지막 제출 시각" + }, + "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { + "defaultMessage": "마지막 표절 검사 이후 새 제출물이 감지되었습니다" + }, + "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { + "defaultMessage": "마지막 표절 검사 이후 새 제출물이 없습니다" + }, + "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { + "defaultMessage": "검사 가능한 질문이 없습니다" + }, + "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { + "defaultMessage": "제출이 충분하지 않습니다" + }, + "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { + "defaultMessage": "# 검사 가능한 질문" }, "course.plagiarism.PlagiarismIndex.assessments.numSubmitted": { "defaultMessage": "# 제출" }, - "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { - "defaultMessage": "# 검사 가능한 질문" + "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { + "defaultMessage": "새 표절 검사 ({count})" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { + "defaultMessage": "표절 검사 실행" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { + "defaultMessage": "일부 평가에 대한 표절 검사를 시작하지 못했습니다" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { + "defaultMessage": "표절 검사를 시작했습니다 {count, plural, =1 {# 평가} other {# 평가}}" + }, + "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { + "defaultMessage": "평가 제목으로 검색" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { + "defaultMessage": "완료됨" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { + "defaultMessage": "실패" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { + "defaultMessage": "시작되지 않음" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { + "defaultMessage": "실행 중" + }, + "course.plagiarism.PlagiarismIndex.assessments.viewResults": { + "defaultMessage": "결과 보기" + }, + "course.plagiarism.PlagiarismIndex.header.plagiarism": { + "defaultMessage": "표절 검사" + }, + "course.statistics.StatisticsIndex.assessments.averageGrade": { + "defaultMessage": "평균 점수" + }, + "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { + "defaultMessage": "평균 소요 시간" }, - "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { - "defaultMessage": "마지막 제출 시각" + "course.statistics.StatisticsIndex.assessments.category": { + "defaultMessage": "카테고리" }, - "course.plagiarism.PlagiarismIndex.assessments.lastRunStatus": { - "defaultMessage": "상태" + "course.statistics.StatisticsIndex.assessments.csvFileTitle": { + "defaultMessage": "평가 통계" }, - "course.plagiarism.PlagiarismIndex.assessments.lastRunTime": { - "defaultMessage": "마지막 실행 시각" + "course.statistics.StatisticsIndex.assessments.downloadCsv": { + "defaultMessage": "다운로드" }, - "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { - "defaultMessage": "시작되지 않음" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummary": { + "defaultMessage": "다음 평가의 점수 요약을 다운로드하시겠습니까?" }, - "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { - "defaultMessage": "실행 중" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { + "defaultMessage": "점수 요약 다운로드 중 오류가 발생했습니다" }, - "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { - "defaultMessage": "완료됨" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { + "defaultMessage": "다운로드를 처리 중입니다. 잠시 기다려주세요." }, - "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { - "defaultMessage": "실패" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { + "defaultMessage": "점수 요약을 성공적으로 다운로드했습니다" }, - "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { - "defaultMessage": "검사 가능한 질문이 없습니다" + "course.statistics.StatisticsIndex.assessments.numAttemptedStudents": { + "defaultMessage": "시도한 학생 수" }, - "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { - "defaultMessage": "제출이 충분하지 않습니다" + "course.statistics.StatisticsIndex.assessments.numLateStudents": { + "defaultMessage": "지각한 학생 수" }, - "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { - "defaultMessage": "새 표절 검사 ({count})" + "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { + "defaultMessage": "제출한 학생 수" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { - "defaultMessage": "표절 검사를 시작했습니다 {count, plural, =1 {# 평가} other {# 평가}}" + "course.statistics.StatisticsIndex.assessments.searchBar": { + "defaultMessage": "평가 제목, 탭 또는 카테고리로 검색" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { - "defaultMessage": "일부 평가에 대한 표절 검사를 시작하지 못했습니다" + "course.statistics.StatisticsIndex.assessments.selectedNUsers": { + "defaultMessage": "{numUsers}명 학생의 점수 요약을 다운로드하시겠습니까?" }, - "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { - "defaultMessage": "평가 제목으로 검색" + "course.statistics.StatisticsIndex.assessments.startAt": { + "defaultMessage": "시작 시간" }, - "course.plagiarism.PlagiarismIndex.assessments.actions": { - "defaultMessage": "작업" + "course.statistics.StatisticsIndex.assessments.stdevGrade": { + "defaultMessage": "점수 표준편차" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { - "defaultMessage": "표절 검사 실행" + "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { + "defaultMessage": "소요 시간 표준편차" }, - "course.plagiarism.PlagiarismIndex.assessments.viewResults": { - "defaultMessage": "결과 보기" + "course.statistics.StatisticsIndex.assessments.subtitle": { + "defaultMessage": "개별 학생 성적을 확인하고 내보내려면 성적부를 여세요." }, - "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { - "defaultMessage": "마지막 표절 검사 이후 새 제출물이 감지되었습니다" + "course.statistics.StatisticsIndex.assessments.subtitleDisabled": { + "defaultMessage": "개별 학생 성적을 확인하고 내보내려면 성적부을 활성화하세요." }, - "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { - "defaultMessage": "마지막 표절 검사 이후 새 제출물이 없습니다" + "course.statistics.StatisticsIndex.assessments.tab": { + "defaultMessage": "탭" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { - "defaultMessage": "표절 검사를 확인하시겠습니까?" + "course.statistics.StatisticsIndex.assessments.tableTitle": { + "defaultMessage": "평가 통계 ({numStudents}명의 학생)" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { - "defaultMessage": "선택한 평가 중 일부는 이미 완료된 표절 검사가 있습니다. 새 표절 검사를 실행하면 이전 결과가 제거됩니다." + "course.statistics.StatisticsIndex.assessments.title": { + "defaultMessage": "제목" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.achievementCount": { "defaultMessage": "성과 개수 (총계: {courseAchievementCount})" @@ -6146,9 +6560,6 @@ "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoSubmissionCountHeader": { "defaultMessage": "시청한 비디오 (총계: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.course.searchBar": { - "defaultMessage": "학생 이름으로 검색" - }, "course.statistics.StatisticsIndex.course.StudentProgressionChart.deadlines": { "defaultMessage": "마감일" }, @@ -6188,78 +6599,18 @@ "course.statistics.StatisticsIndex.course.progressionError": { "defaultMessage": "과정 진행 통계를 가져오는 동안 문제가 발생했습니다! 새로고침해서 다시 시도하세요." }, + "course.statistics.StatisticsIndex.course.searchBar": { + "defaultMessage": "학생 이름으로 검색" + }, "course.statistics.StatisticsIndex.header.statistics": { "defaultMessage": "통계" }, - "course.statistics.StatisticsIndex.assessments.title": { - "defaultMessage": "제목" - }, - "course.statistics.StatisticsIndex.assessments.startAt": { - "defaultMessage": "시작 시간" - }, - "course.statistics.StatisticsIndex.assessments.tab": { - "defaultMessage": "탭" - }, - "course.statistics.StatisticsIndex.assessments.category": { - "defaultMessage": "카테고리" - }, - "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { - "defaultMessage": "제출한 학생 수" - }, - "course.statistics.StatisticsIndex.assessments.numAttemptedStudents": { - "defaultMessage": "시도한 학생 수" - }, - "course.statistics.StatisticsIndex.assessments.numLateStudents": { - "defaultMessage": "지각한 학생 수" - }, - "course.statistics.StatisticsIndex.assessments.averageGrade": { - "defaultMessage": "평균 점수" - }, - "course.statistics.StatisticsIndex.assessments.stdevGrade": { - "defaultMessage": "점수 표준편차" - }, - "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { - "defaultMessage": "평균 소요 시간" - }, - "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { - "defaultMessage": "소요 시간 표준편차" - }, - "course.statistics.StatisticsIndex.assessments.subtitle": { - "defaultMessage": "개별 학생 성적을 확인하고 내보내려면 성적부를 여세요." - }, - "course.statistics.StatisticsIndex.assessments.subtitleDisabled": { - "defaultMessage": "개별 학생 성적을 확인하고 내보내려면 성적부을 활성화하세요." - }, - "course.statistics.StatisticsIndex.assessments.tableTitle": { - "defaultMessage": "평가 통계 ({numStudents}명의 학생)" - }, - "course.statistics.StatisticsIndex.assessments.csvFileTitle": { - "defaultMessage": "평가 통계" - }, - "course.statistics.StatisticsIndex.assessments.searchBar": { - "defaultMessage": "평가 제목, 탭 또는 카테고리로 검색" - }, - "course.statistics.StatisticsIndex.assessments.selectedNUsers": { - "defaultMessage": "{numUsers}명 학생의 점수 요약을 다운로드하시겠습니까?" - }, - "course.statistics.StatisticsIndex.assessments.downloadCsv": { - "defaultMessage": "다운로드" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummary": { - "defaultMessage": "다음 평가의 점수 요약을 다운로드하시겠습니까?" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { - "defaultMessage": "점수 요약을 성공적으로 다운로드했습니다" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { - "defaultMessage": "점수 요약 다운로드 중 오류가 발생했습니다" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { - "defaultMessage": "다운로드를 처리 중입니다. 잠시 기다려주세요." - }, "course.statistics.StatisticsIndex.staff.averageMarkingTime": { "defaultMessage": "평균 평가 시간" }, + "course.statistics.StatisticsIndex.staff.csvFileTitle": { + "defaultMessage": "교직원 통계" + }, "course.statistics.StatisticsIndex.staff.error": { "defaultMessage": "교직원 통계를 가져오는 동안 문제가 발생했습니다! 새로고침해서 다시 시도하세요." }, @@ -6272,21 +6623,24 @@ "course.statistics.StatisticsIndex.staff.numStudents": { "defaultMessage": "# 학생" }, + "course.statistics.StatisticsIndex.staff.searchBar": { + "defaultMessage": "교직원 이름으로 검색" + }, "course.statistics.StatisticsIndex.staff.stddev": { "defaultMessage": "표준 편차" }, "course.statistics.StatisticsIndex.staff.tableTitle": { "defaultMessage": "교직원 통계" }, - "course.statistics.StatisticsIndex.staff.csvFileTitle": { - "defaultMessage": "교직원 통계" - }, - "course.statistics.StatisticsIndex.staff.searchBar": { - "defaultMessage": "교직원 이름으로 검색" - }, "course.statistics.StatisticsIndex.staffFailure": { "defaultMessage": "교직원 데이터를 가져오는 데 실패했습니다!" }, + "course.statistics.StatisticsIndex.students.csvFileTitle": { + "defaultMessage": "학생 통계" + }, + "course.statistics.StatisticsIndex.students.email": { + "defaultMessage": "이메일" + }, "course.statistics.StatisticsIndex.students.error": { "defaultMessage": "학생 통계를 가져오는 동안 문제가 발생했습니다! 새로고침해서 다시 시도하세요." }, @@ -6302,12 +6656,12 @@ "course.statistics.StatisticsIndex.students.name": { "defaultMessage": "이름" }, - "course.statistics.StatisticsIndex.students.email": { - "defaultMessage": "이메일" - }, "course.statistics.StatisticsIndex.students.noStudents": { "defaultMessage": "아직 이 과정에 학생이 없습니다..." }, + "course.statistics.StatisticsIndex.students.searchBar": { + "defaultMessage": "학생 이름, 학생 유형 또는 외부 ID로 검색" + }, "course.statistics.StatisticsIndex.students.showMyStudentsOnly": { "defaultMessage": "내 학생만 보기" }, @@ -6326,12 +6680,6 @@ "course.statistics.StatisticsIndex.students.videoSubmissionCount": { "defaultMessage": "시청한 비디오 (총계: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.students.csvFileTitle": { - "defaultMessage": "학생 통계" - }, - "course.statistics.StatisticsIndex.students.searchBar": { - "defaultMessage": "학생 이름, 학생 유형 또는 외부 ID로 검색" - }, "course.statistics.StatisticsIndex.studentsFailure": { "defaultMessage": "학생 데이터를 가져오는 데 실패했습니다!" }, @@ -7022,6 +7370,27 @@ "course.userInvitation.InviteUsersRegistrationCode.registrationCodeNote": { "defaultMessage": "초대받은 사용자가 이 초대 코드를 사용하여 과정에 등록할 경우, 초대 페이지에서 해당 상태가 올바르게 반영되지 않을 수 있습니다." }, + "course.userInvitations.ExternalIdConflictPrompt.body": { + "defaultMessage": "이 사용자들은 이미 등록되었거나 초대가 대기 중입니다. 새로운 초대 이메일은 발송되지 않습니다. 현재 외부 ID를 유지하거나 파일의 값으로 교체하시겠습니까?" + }, + "course.userInvitations.ExternalIdConflictPrompt.goBack": { + "defaultMessage": "돌아가기" + }, + "course.userInvitations.ExternalIdConflictPrompt.keepExisting": { + "defaultMessage": "현재 유지" + }, + "course.userInvitations.ExternalIdConflictPrompt.pendingCourseUserUpdates": { + "defaultMessage": "대기 중인 수강생 업데이트 ({count}개)" + }, + "course.userInvitations.ExternalIdConflictPrompt.pendingInvitationUpdates": { + "defaultMessage": "대기 중인 초대 업데이트 ({count}개)" + }, + "course.userInvitations.ExternalIdConflictPrompt.replace": { + "defaultMessage": "교체" + }, + "course.userInvitations.ExternalIdConflictPrompt.title": { + "defaultMessage": "외부 ID 업데이트 확인" + }, "course.userInvitations.IndividualInvitations.appendNewRow": { "defaultMessage": "행 추가" }, @@ -7037,6 +7406,33 @@ "course.userInvitations.IndividualInvitations.removeInvitation": { "defaultMessage": "초대 삭제" }, + "course.userInvitations.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "{name} ({email})에 대한 초대를 삭제하시겠습니까?" + }, + "course.userInvitations.InvitationActionButtons.deletionFailure": { + "defaultMessage": "사용자 삭제에 실패했습니다 - {error}" + }, + "course.userInvitations.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "{name}에 대한 초대가 삭제되었습니다." + }, + "course.userInvitations.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "초대 삭제" + }, + "course.userInvitations.InvitationActionButtons.resendFailure": { + "defaultMessage": "초대 재전송에 실패했습니다." + }, + "course.userInvitations.InvitationActionButtons.resendSuccess": { + "defaultMessage": "{email}로 초대 이메일을 다시 보냈습니다!" + }, + "course.userInvitations.InvitationActionButtons.resendTooltip": { + "defaultMessage": "초대 재전송" + }, + "course.userInvitations.InvitationResultDialog.actionableTitle": { + "defaultMessage": "실패 ({count})" + }, + "course.userInvitations.InvitationResultDialog.blankHeaderWarning": { + "defaultMessage": "헤더가 없는 열이 하나 이상 있어 해당 데이터는 무시되었습니다." + }, "course.userInvitations.InvitationResultDialog.body": { "defaultMessage": "{newInvitationsCount, plural, =0 {새로운 사용자가 없습니다.} one {새 사용자 #명이} other {새 사용자 #명이}} Coursemology에 초대되었습니다. {newCourseUsersCount, plural, =0 {Coursemology 계정을 가진 사용자가 없습니다.} one {Coursemology 계정을 가진 새로운 사용자가 #명이} other {Coursemology 계정을 가진 새로운 사용자가 #명이}} 이 과정에 추가되었습니다." }, @@ -7055,15 +7451,24 @@ "course.userInvitations.InvitationResultDialog.existingCourseUsersInfo": { "defaultMessage": "이 사용자들은 이미 과정에 등록되어 있습니다. 재등록되지 않습니다." }, - "course.userInvitations.InvitationResultDialog.externalIdUpdatedInfo": { - "defaultMessage": "외부 ID가 지정된 경우 업데이트되었습니다." - }, "course.userInvitations.InvitationResultDialog.existingInvitations": { "defaultMessage": "기존 초대장 ({count})" }, "course.userInvitations.InvitationResultDialog.existingInvitationsInfo": { "defaultMessage": "이 사용자들은 이미 대기 중인 초대장이 있습니다. 재초대되지 않습니다." }, + "course.userInvitations.InvitationResultDialog.externalIdUpdatedInfo": { + "defaultMessage": "외부 ID가 지정된 경우 업데이트되었습니다." + }, + "course.userInvitations.InvitationResultDialog.failedInvitations": { + "defaultMessage": "전송 실패 ({count})" + }, + "course.userInvitations.InvitationResultDialog.failedInvitationsInfo": { + "defaultMessage": "초대 이메일 전송 중 오류가 발생했습니다. 다시 시도해 주세요." + }, + "course.userInvitations.InvitationResultDialog.failedRowsSubtitle": { + "defaultMessage": "빨간색으로 강조된 {count}개 행 전송 실패" + }, "course.userInvitations.InvitationResultDialog.header": { "defaultMessage": "초대 요약" }, @@ -7071,16 +7476,10 @@ "defaultMessage": "새 과정 사용자 ({count})" }, "course.userInvitations.InvitationResultDialog.newInvitations": { - "defaultMessage": "새 초대장 ({count})" - }, - "course.userInvitations.InvitationResultDialog.actionableTitle": { - "defaultMessage": "실패 ({count})" - }, - "course.userInvitations.InvitationResultDialog.failedInvitations": { - "defaultMessage": "전송 실패 ({count})" + "defaultMessage": "새 초대장 ({count})" }, - "course.userInvitations.InvitationResultDialog.failedInvitationsInfo": { - "defaultMessage": "초대 이메일 전송 중 오류가 발생했습니다. 다시 시도해 주세요." + "course.userInvitations.InvitationResultDialog.summary": { + "defaultMessage": "새 초대장 {newInvitations}개 발송, {newEnrollments}명 직접 등록, {alreadyInCourse}명은 이미 과정에 있습니다." }, "course.userInvitations.InvitationResultDialog.summaryFailed": { "defaultMessage": "{count}건이 실패했습니다." @@ -7088,14 +7487,8 @@ "course.userInvitations.InvitationResultDialog.updatedSubtitle": { "defaultMessage": "{count}개 업데이트됨 · 먼저 표시" }, - "course.userInvitations.InvitationResultDialog.blankHeaderWarning": { - "defaultMessage": "헤더가 없는 열이 하나 이상 있어 해당 데이터는 무시되었습니다." - }, - "course.userInvitations.InvitationResultDialog.summary": { - "defaultMessage": "새 초대장 {newInvitations}개 발송, {newEnrollments}명 직접 등록, {alreadyInCourse}명은 이미 과정에 있습니다." - }, - "course.userInvitations.InvitationResultDialog.failedRowsSubtitle": { - "defaultMessage": "빨간색으로 강조된 {count}개 행 전송 실패" + "course.userInvitations.InvitationResultExistingTable.previouslyLabel": { + "defaultMessage": "이전 값: {value}" }, "course.userInvitations.InvitationResultFailedTable.duplicateEmailInFile": { "defaultMessage": "업로드 파일에 중복된 이메일 주소가 있습니다" @@ -7106,18 +7499,15 @@ "course.userInvitations.InvitationResultFailedTable.externalIdTaken": { "defaultMessage": "해당 외부 ID는 이미 다른 과정 구성원에게 할당되어 있습니다" }, + "course.userInvitations.InvitationResultFailedTable.externalIdTakenEnrolled": { + "defaultMessage": "이미 과정 구성원입니다 - 외부 ID를 적용할 수 없습니다(이미 다른 구성원에게 할당됨)" + }, "course.userInvitations.InvitationResultFailedTable.failedToSend": { "defaultMessage": "초대 이메일 전송에 실패했습니다. 다시 시도해 주세요 - 문제가 지속되면 저희에게 문의해 주세요" }, - "course.userInvitations.InvitationResultFailedTable.externalIdTakenEnrolled": { - "defaultMessage": "이미 과정 구성원입니다 — 외부 ID를 적용할 수 없습니다(이미 다른 구성원에게 할당됨)" - }, "course.userInvitations.InvitationResultSkippedTable.previouslyLabel": { "defaultMessage": "이전 값: {value}" }, - "course.userInvitations.InvitationResultExistingTable.previouslyLabel": { - "defaultMessage": "이전 값: {value}" - }, "course.userInvitations.InvitationsBarChart.accepted": { "defaultMessage": "수락된 초대" }, @@ -7154,14 +7544,17 @@ "course.userInvitations.InviteUsersFileUpload.failureGeneric": { "defaultMessage": "사용자 초대에 실패했습니다. 데이터가 올바르게 형식화되었는지 확인하세요." }, + "course.userInvitations.InviteUsersFileUpload.fileRequired": { + "defaultMessage": "업로드할 CSV 파일을 선택해 주세요." + }, "course.userInvitations.InviteUsersFileUpload.fileUploadExample": { "defaultMessage": "이름,이메일,외부 ID,역할,팬텀{br}John,test1@example.com,A0123456,student,y{br}Mary,test2@example.com,A0123457,teaching_assistant,n" }, "course.userInvitations.InviteUsersFileUpload.fileUploadExamplePersonalTimeline": { "defaultMessage": "이름,이메일,외부 ID,역할,팬텀,개인 타임라인{br}John,test1@example.com,A0123456,student,y,otot{br}Mary,test2@example.com,A0123457,teaching_assistant,n,fixed" }, - "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRequired": { - "defaultMessage": "CSV에는 \"이름\"과 \"이메일\" 열이 모두 포함되어야 합니다. 다른 모든 열은 선택 사항입니다." + "course.userInvitations.InviteUsersFileUpload.fileUploadInfo": { + "defaultMessage": "다음 형식으로 .csv 파일을 업로드하세요:" }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoEmail": { "defaultMessage": "각 초대는 과정 내에서 고유한 이메일 주소를 사용해야 합니다. 중복된 이메일은 건너뜁니다." @@ -7169,24 +7562,21 @@ "course.userInvitations.InviteUsersFileUpload.fileUploadInfoExternalId": { "defaultMessage": "외부 ID가 제공된 경우, 해당 외부 ID는 과정 내에서 고유해야 합니다." }, - "course.userInvitations.InviteUsersFileUpload.fileUploadInfo": { - "defaultMessage": "다음 형식으로 .csv 파일을 업로드하세요:" - }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoPersonalTimeline": { "defaultMessage": "개인 타임라인은 [fixed, otot, stragglers, fomo]로 설정할 수 있으며, 생략 시 과정 기본값: {defaultTimelineAlgorithm} 입니다." }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoPhantom": { "defaultMessage": "팬텀은 true/false로 설정할 수 있으며, true 값은 ['t', 'true', 'y', 'yes'] (대소문자 무관)으로 지정됩니다. 생략 시 기본값은 false입니다." }, + "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRequired": { + "defaultMessage": "CSV에는 \"이름\"과 \"이메일\" 열이 모두 포함되어야 합니다. 다른 모든 열은 선택 사항입니다." + }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRole": { "defaultMessage": "역할은 [학생, 관찰자, 티칭 어시스턴트, 관리자, 소유자]로 설정할 수 있으며, 생략 시 기본값은 학생입니다. 티칭 어시스턴트는 사용자 초대를 학생으로만 할 수 있습니다." }, "course.userInvitations.InviteUsersFileUpload.importInProgress": { "defaultMessage": "사용자를 가져오는 중입니다. 잠시만 기다려 주세요…" }, - "course.userInvitations.InviteUsersFileUpload.fileRequired": { - "defaultMessage": "업로드할 CSV 파일을 선택해 주세요." - }, "course.userInvitations.InviteUsersFileUpload.template": { "defaultMessage": "(템플릿 파일)" }, @@ -7199,27 +7589,6 @@ "course.userInvitations.InviteUsersfileUploadForm.invite": { "defaultMessage": "파일에서 사용자 초대" }, - "course.userInvitations.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "{name} ({email})에 대한 초대를 삭제하시겠습니까?" - }, - "course.userInvitations.InvitationActionButtons.deletionFailure": { - "defaultMessage": "사용자 삭제에 실패했습니다 - {error}" - }, - "course.userInvitations.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "{name}에 대한 초대가 삭제되었습니다." - }, - "course.userInvitations.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "초대 삭제" - }, - "course.userInvitations.InvitationActionButtons.resendFailure": { - "defaultMessage": "초대 재전송에 실패했습니다." - }, - "course.userInvitations.InvitationActionButtons.resendSuccess": { - "defaultMessage": "{email}로 초대 이메일을 다시 보냈습니다!" - }, - "course.userInvitations.InvitationActionButtons.resendTooltip": { - "defaultMessage": "초대 재전송" - }, "course.userInvitations.RegistrationCodeButton.registrationCode": { "defaultMessage": "등록 코드" }, @@ -7238,24 +7607,24 @@ "course.userInvitations.UserInvitationsTable.accepted": { "defaultMessage": "수락됨" }, + "course.userInvitations.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "{confirmedAt}에 수락됨" + }, "course.userInvitations.UserInvitationsTable.failed": { "defaultMessage": "실패" }, "course.userInvitations.UserInvitationsTable.noInvitations": { "defaultMessage": "초대가 없습니다." }, - "course.userInvitations.UserInvitationsTable.searchText": { - "defaultMessage": "이름, 이메일 또는 외부 ID로 검색" - }, "course.userInvitations.UserInvitationsTable.pending": { "defaultMessage": "대기 중" }, + "course.userInvitations.UserInvitationsTable.searchText": { + "defaultMessage": "이름, 이메일 또는 외부 ID로 검색" + }, "course.userInvitations.UserInvitationsTable.sentTooltip": { "defaultMessage": "{sentAt}에 전송됨" }, - "course.userInvitations.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "{confirmedAt}에 수락됨" - }, "course.userNotification.AchievementGainedPopup.unlocked": { "defaultMessage": "업적 달성!" }, @@ -7292,6 +7661,12 @@ "course.users.ManageUsersTable.ManageUsersTable.searchText": { "defaultMessage": "이름, 이메일 또는 외부 ID로 검색" }, + "course.users.ManageUsersTable.addIdFailure": { + "defaultMessage": "외부 ID를 {newId}로 설정하지 못했습니다." + }, + "course.users.ManageUsersTable.addIdSuccess": { + "defaultMessage": "외부 ID가 {newId}로 설정되었습니다." + }, "course.users.ManageUsersTable.assignToTimeline": { "defaultMessage": "타임라인에 할당" }, @@ -7322,6 +7697,12 @@ "course.users.ManageUsersTable.changeAlgorithmSuccess": { "defaultMessage": "{name}의 타임라인 알고리즘을 {timeline}(으)로 업데이트했습니다." }, + "course.users.ManageUsersTable.changeIdFailure": { + "defaultMessage": "ID를 {oldId}에서 {newId}로 변경하지 못했습니다." + }, + "course.users.ManageUsersTable.changeIdSuccess": { + "defaultMessage": "ID가 {oldId}에서 {newId}로 변경되었습니다." + }, "course.users.ManageUsersTable.changeRoleFailure": { "defaultMessage": "{name}의 역할을 {role}(으)로 업데이트하지 못했습니다." }, @@ -7337,29 +7718,29 @@ "course.users.ManageUsersTable.defaultTimeline": { "defaultMessage": "기본값" }, - "course.users.ManageUsersTable.group": { - "defaultMessage": "그룹: {name}" + "course.users.ManageUsersTable.deleteIdFailure": { + "defaultMessage": "외부 ID를 삭제하지 못했습니다." }, - "course.users.ManageUsersTable.phantomSuccess": { - "defaultMessage": "{name} {isPhantom, select, true {은(는) 이제 팬텀 사용자입니다} other {은(는) 이제 일반 사용자입니다}}." + "course.users.ManageUsersTable.deleteIdSuccess": { + "defaultMessage": "외부 ID가 삭제되었습니다." }, - "course.users.ManageUsersTable.addIdFailure": { - "defaultMessage": "외부 ID를 {newId}로 설정하지 못했습니다." + "course.users.ManageUsersTable.deletionConfirm": { + "defaultMessage": "{role} {name} ({email})을(를) 삭제하시겠습니까?" }, - "course.users.ManageUsersTable.addIdSuccess": { - "defaultMessage": "외부 ID가 {newId}로 설정되었습니다." + "course.users.ManageUsersTable.deletionFailure": { + "defaultMessage": "사용자 삭제에 실패했습니다." }, - "course.users.ManageUsersTable.changeIdFailure": { - "defaultMessage": "ID를 {oldId}에서 {newId}로 변경하지 못했습니다." + "course.users.ManageUsersTable.deletionScheduled": { + "defaultMessage": "{role} {name} ({email}) 삭제가 예약되었습니다." }, - "course.users.ManageUsersTable.changeIdSuccess": { - "defaultMessage": "ID가 {oldId}에서 {newId}로 변경되었습니다." + "course.users.ManageUsersTable.deletionSuccess": { + "defaultMessage": "사용자가 삭제되었습니다." }, - "course.users.ManageUsersTable.deleteIdFailure": { - "defaultMessage": "외부 ID를 삭제하지 못했습니다." + "course.users.ManageUsersTable.group": { + "defaultMessage": "그룹: {name}" }, - "course.users.ManageUsersTable.deleteIdSuccess": { - "defaultMessage": "외부 ID가 삭제되었습니다." + "course.users.ManageUsersTable.phantomSuccess": { + "defaultMessage": "{name} {isPhantom, select, true {은(는) 이제 팬텀 사용자입니다} other {은(는) 이제 일반 사용자입니다}}." }, "course.users.ManageUsersTable.renameFailure": { "defaultMessage": "{oldName}을(를) {newName}(으)로 이름 변경하지 못했습니다." @@ -7370,6 +7751,24 @@ "course.users.ManageUsersTable.selectedNUsers": { "defaultMessage": "{n, plural, =1 {#명의 사용자} other {#명의 사용자들}}이 선택되었습니다." }, + "course.users.ManageUsersTable.suspend": { + "defaultMessage": "정지" + }, + "course.users.ManageUsersTable.suspendFailure": { + "defaultMessage": "{name}을(를) 정지하지 못했습니다." + }, + "course.users.ManageUsersTable.suspendSuccess": { + "defaultMessage": "{name}이(가) 이제 정지되었습니다. 정지가 해제될 때까지 이 과정에 접근할 수 없습니다." + }, + "course.users.ManageUsersTable.unsuspend": { + "defaultMessage": "정지 해제" + }, + "course.users.ManageUsersTable.unsuspendFailure": { + "defaultMessage": "{name}의 정지를 해제하지 못했습니다." + }, + "course.users.ManageUsersTable.unsuspendSuccess": { + "defaultMessage": "{name}의 정지가 해제되었습니다. 이제 과정에 접근할 수 있습니다." + }, "course.users.ManageUsersTable.updateFailure": { "defaultMessage": "사용자 업데이트에 실패했습니다 - {error}" }, @@ -7478,36 +7877,6 @@ "course.users.UpgradeToStaff.upgradeSuccess": { "defaultMessage": "{count, plural, =0 {사용자가 없습니다.} one {#명의 사용자가} other {#명의 사용자가}} {role}(으)로 승격되었습니다." }, - "course.users.ManageUsersTable.deletionConfirm": { - "defaultMessage": "{role} {name} ({email})을(를) 삭제하시겠습니까?" - }, - "course.users.ManageUsersTable.deletionFailure": { - "defaultMessage": "사용자 삭제에 실패했습니다." - }, - "course.users.ManageUsersTable.deletionScheduled": { - "defaultMessage": "{role} {name} ({email}) 삭제가 예약되었습니다." - }, - "course.users.ManageUsersTable.deletionSuccess": { - "defaultMessage": "사용자가 삭제되었습니다." - }, - "course.users.ManageUsersTable.suspend": { - "defaultMessage": "정지" - }, - "course.users.ManageUsersTable.suspendFailure": { - "defaultMessage": "{name}을(를) 정지하지 못했습니다." - }, - "course.users.ManageUsersTable.suspendSuccess": { - "defaultMessage": "{name}이(가) 이제 정지되었습니다. 정지가 해제될 때까지 이 과정에 접근할 수 없습니다." - }, - "course.users.ManageUsersTable.unsuspend": { - "defaultMessage": "정지 해제" - }, - "course.users.ManageUsersTable.unsuspendFailure": { - "defaultMessage": "{name}의 정지를 해제하지 못했습니다." - }, - "course.users.ManageUsersTable.unsuspendSuccess": { - "defaultMessage": "{name}의 정지가 해제되었습니다. 이제 과정에 접근할 수 있습니다." - }, "course.users.UserManagementTabs.enrolRequestsTitle": { "defaultMessage": "등록 요청" }, @@ -7646,27 +8015,27 @@ "course.video.VideoShow.videoTitle": { "defaultMessage": "비디오 - {title}" }, + "course.video.VideoTable.actions": { + "defaultMessage": "작업" + }, + "course.video.VideoTable.averageWatched": { + "defaultMessage": "평균 시청 비율" + }, "course.video.VideoTable.noVideo": { "defaultMessage": "비디오 없음" }, - "course.video.VideoTable.title": { - "defaultMessage": "제목" + "course.video.VideoTable.published": { + "defaultMessage": "게시됨" }, "course.video.VideoTable.startAt": { "defaultMessage": "시작 시간" }, + "course.video.VideoTable.title": { + "defaultMessage": "제목" + }, "course.video.VideoTable.watchCount": { "defaultMessage": "시청 횟수" }, - "course.video.VideoTable.averageWatched": { - "defaultMessage": "평균 시청 비율" - }, - "course.video.VideoTable.published": { - "defaultMessage": "게시됨" - }, - "course.video.VideoTable.actions": { - "defaultMessage": "작업" - }, "course.video.VideosIndex.fetchVideosFailure": { "defaultMessage": "비디오를 가져오지 못했습니다." }, @@ -7853,12 +8222,12 @@ "lib.components.core.Expandable.showMore": { "defaultMessage": "더 보기" }, - "lib.components.core.Note.noteHeader": { - "defaultMessage": "노트" - }, "lib.components.core.Note.errorHeader": { "defaultMessage": "오류" }, + "lib.components.core.Note.noteHeader": { + "defaultMessage": "노트" + }, "lib.components.core.banners.ServerUnreachableBanner.refreshPage": { "defaultMessage": "페이지 새로고침" }, @@ -7964,29 +8333,95 @@ "lib.components.extensions.conditions.scoreZeroPercentNotice": { "defaultMessage": "0% 이상 점수는 이 평가가 채점된 이후에 조건이 충족되도록 요구합니다. 최소 점수가 지정되지 않은 경우, 이 조건은 단순 제출만 요구합니다." }, - "lib.components.extensions.conditions.scoringAtLeast": { - "defaultMessage": "최소 점수" + "lib.components.extensions.conditions.scoringAtLeast": { + "defaultMessage": "최소 점수" + }, + "lib.components.extensions.conditions.specifyLevel": { + "defaultMessage": "최소 레벨 지정" + }, + "lib.components.extensions.conditions.survey": { + "defaultMessage": "설문 조사" + }, + "lib.components.extensions.conditions.type": { + "defaultMessage": "유형" + }, + "lib.components.extensions.conditions.updateCondition": { + "defaultMessage": "조건 업데이트" + }, + "lib.components.form.fields.DateTimePickerField.invalidDateTime": { + "defaultMessage": "잘못된 날짜 및/또는 시간" + }, + "lib.components.form.fields.SingleFileInput.dropzone": { + "defaultMessage": "여기에 파일을 끌어놓거나 클릭하여 파일을 선택하세요." + }, + "lib.components.form.fields.SingleFileInput.removeFile": { + "defaultMessage": "파일 제거" + }, + "lib.components.getHelp.filter.filterAssessmentLabel": { + "defaultMessage": "평가로 필터링" + }, + "lib.components.getHelp.filter.filterCourseLabel": { + "defaultMessage": "과정으로 필터링" + }, + "lib.components.getHelp.filter.filterEndDateLabel": { + "defaultMessage": "종료 날짜" + }, + "lib.components.getHelp.filter.filterStartDateLabel": { + "defaultMessage": "시작 날짜" + }, + "lib.components.getHelp.filter.filterStudentLabel": { + "defaultMessage": "학생으로 필터링" + }, + "lib.components.getHelp.filter.lastFourteenDays": { + "defaultMessage": "최근 14일" + }, + "lib.components.getHelp.filter.lastSevenDays": { + "defaultMessage": "최근 7일" + }, + "lib.components.getHelp.filter.lastSixMonths": { + "defaultMessage": "최근 6개월" + }, + "lib.components.getHelp.filter.lastThirtyDays": { + "defaultMessage": "최근 30일" + }, + "lib.components.getHelp.filter.lastTwelveMonths": { + "defaultMessage": "최근 12개월" + }, + "lib.components.getHelp.header": { + "defaultMessage": "최근 도움 요청 활동 ({total, plural, other {#개 대화}})" + }, + "lib.components.getHelp.table.assessmentTitle": { + "defaultMessage": "평가" + }, + "lib.components.getHelp.table.courseTitle": { + "defaultMessage": "과정" + }, + "lib.components.getHelp.table.createdAt": { + "defaultMessage": "마지막 메시지 시간" + }, + "lib.components.getHelp.table.instanceTitle": { + "defaultMessage": "인스턴스" }, - "lib.components.extensions.conditions.specifyLevel": { - "defaultMessage": "최소 레벨 지정" + "lib.components.getHelp.table.lastMessage": { + "defaultMessage": "마지막 메시지" }, - "lib.components.extensions.conditions.survey": { - "defaultMessage": "설문 조사" + "lib.components.getHelp.table.messageCount": { + "defaultMessage": "메시지 수" }, - "lib.components.extensions.conditions.type": { - "defaultMessage": "유형" + "lib.components.getHelp.table.questionNumber": { + "defaultMessage": "질문" }, - "lib.components.extensions.conditions.updateCondition": { - "defaultMessage": "조건 업데이트" + "lib.components.getHelp.table.studentName": { + "defaultMessage": "이름" }, - "lib.components.form.fields.DateTimePickerField.invalidDateTime": { - "defaultMessage": "잘못된 날짜 및/또는 시간" + "lib.components.getHelp.validation.endDateBeforeStartDate": { + "defaultMessage": "종료일은 시작일 이후이거나 같아야 합니다" }, - "lib.components.form.fields.SingleFileInput.dropzone": { - "defaultMessage": "여기에 파일을 끌어놓거나 클릭하여 파일을 선택하세요." + "lib.components.getHelp.validation.exceedDateRange": { + "defaultMessage": "날짜 범위는 365일을 초과할 수 없습니다" }, - "lib.components.form.fields.SingleFileInput.removeFile": { - "defaultMessage": "파일 제거" + "lib.components.getHelp.validation.invalidDateSelection": { + "defaultMessage": "잘못된 날짜" }, "lib.components.navigation.AdminPopupMenuList.adminPanel": { "defaultMessage": "시스템 관리자 패널" @@ -8024,6 +8459,21 @@ "lib.components.navigation.CourseSwitcherPopupMenu.thisCourse": { "defaultMessage": "이 과정" }, + "lib.components.table.MuiColumnPickerPrompt.apply": { + "defaultMessage": "뷰에 적용" + }, + "lib.components.table.MuiColumnPickerPrompt.cancel": { + "defaultMessage": "취소" + }, + "lib.components.table.MuiColumnPickerPrompt.defaultTitle": { + "defaultMessage": "열 선택" + }, + "lib.components.table.MuiTableToolbar.directExport": { + "defaultMessage": "내보내기" + }, + "lib.components.table.MuiTableToolbar.exportTrigger": { + "defaultMessage": "내보내기…" + }, "lib.hooks.router.usePrompt.sureYouWantToLeave": { "defaultMessage": "이 페이지를 떠나시겠습니까? 저장되지 않은 변경 사항이 손실됩니다." }, @@ -8051,108 +8501,27 @@ "lib.translations.beta": { "defaultMessage": "베타" }, - "lib.components.getHelp.header": { - "defaultMessage": "최근 도움 요청 활동 ({total, plural, other {#개 대화}})" - }, - "lib.components.getHelp.filter.filterCourseLabel": { - "defaultMessage": "과정으로 필터링" - }, - "lib.components.getHelp.filter.filterAssessmentLabel": { - "defaultMessage": "평가로 필터링" - }, - "lib.components.getHelp.filter.filterStudentLabel": { - "defaultMessage": "학생으로 필터링" - }, - "lib.components.getHelp.filter.filterStartDateLabel": { - "defaultMessage": "시작 날짜" - }, - "lib.components.getHelp.filter.filterEndDateLabel": { - "defaultMessage": "종료 날짜" - }, - "lib.components.getHelp.filter.lastSevenDays": { - "defaultMessage": "최근 7일" - }, - "lib.components.getHelp.filter.lastFourteenDays": { - "defaultMessage": "최근 14일" - }, - "lib.components.getHelp.filter.lastThirtyDays": { - "defaultMessage": "최근 30일" - }, - "lib.components.getHelp.filter.lastSixMonths": { - "defaultMessage": "최근 6개월" - }, - "lib.components.getHelp.filter.lastTwelveMonths": { - "defaultMessage": "최근 12개월" - }, - "lib.components.getHelp.table.studentName": { - "defaultMessage": "이름" - }, - "lib.components.getHelp.table.messageCount": { - "defaultMessage": "메시지 수" - }, - "lib.components.getHelp.table.lastMessage": { - "defaultMessage": "마지막 메시지" - }, - "lib.components.getHelp.table.questionNumber": { - "defaultMessage": "질문" - }, - "lib.components.getHelp.table.assessmentTitle": { - "defaultMessage": "평가" - }, - "lib.components.getHelp.table.createdAt": { - "defaultMessage": "마지막 메시지 시간" - }, - "lib.components.getHelp.table.courseTitle": { - "defaultMessage": "과정" - }, - "lib.components.getHelp.table.instanceTitle": { - "defaultMessage": "인스턴스" - }, - "lib.components.getHelp.validation.invalidDateSelection": { - "defaultMessage": "잘못된 날짜" - }, - "lib.components.getHelp.validation.endDateBeforeStartDate": { - "defaultMessage": "종료일은 시작일 이후이거나 같아야 합니다" - }, - "lib.components.getHelp.validation.exceedDateRange": { - "defaultMessage": "날짜 범위는 365일을 초과할 수 없습니다" - }, - "lib.components.table.MuiColumnPickerPrompt.apply": { - "defaultMessage": "뷰에 적용" - }, - "lib.components.table.MuiColumnPickerPrompt.cancel": { - "defaultMessage": "취소" - }, - "lib.components.table.MuiColumnPickerPrompt.defaultTitle": { - "defaultMessage": "열 선택" - }, - "lib.components.table.MuiTableToolbar.directExport": { - "defaultMessage": "내보내기" - }, - "lib.components.table.MuiTableToolbar.exportTrigger": { - "defaultMessage": "내보내기…" - }, "lib.translations.course.users.fetchUsersFailure": { "defaultMessage": "사용자를 가져오는 데 실패했습니다." }, "lib.translations.course.users.manageUsersHeader": { "defaultMessage": "사용자 관리" }, - "lib.translations.course.users.roles.student": { - "defaultMessage": "학생" - }, - "lib.translations.course.users.roles.teachingAssistant": { - "defaultMessage": "조교" + "lib.translations.course.users.roles.manager": { + "defaultMessage": "관리자" }, "lib.translations.course.users.roles.observer": { "defaultMessage": "관찰자" }, - "lib.translations.course.users.roles.manager": { - "defaultMessage": "관리자" - }, "lib.translations.course.users.roles.owner": { "defaultMessage": "소유자" }, + "lib.translations.course.users.roles.student": { + "defaultMessage": "학생" + }, + "lib.translations.course.users.roles.teachingAssistant": { + "defaultMessage": "조교" + }, "lib.translations.experimental": { "defaultMessage": "실험적" }, @@ -8228,24 +8597,6 @@ "lib.translations.form.messages.unsavedChanges": { "defaultMessage": "저장되지 않은 변경 사항이 있습니다." }, - "lib.translations.instance.users.roles.normal": { - "defaultMessage": "일반 사용자" - }, - "lib.translations.instance.users.roles.instructor": { - "defaultMessage": "교사" - }, - "lib.translations.instance.users.roles.administrator": { - "defaultMessage": "관리자" - }, - "lib.translations.myStudentsIncludingPhantoms": { - "defaultMessage": "내 학생들 (팬텀 포함)" - }, - "lib.translations.studentsIncludingPhantoms": { - "defaultMessage": "학생들 (팬텀 포함)" - }, - "lib.translations.staffIncludingPhantoms": { - "defaultMessage": "스태프 (팬텀 포함)" - }, "lib.translations.form.startAt": { "defaultMessage": "시작 시각" }, @@ -8282,6 +8633,15 @@ "lib.translations.form.validation.startEndDateValidationError": { "defaultMessage": "시작 날짜 이후여야 합니다." }, + "lib.translations.instance.users.roles.administrator": { + "defaultMessage": "관리자" + }, + "lib.translations.instance.users.roles.instructor": { + "defaultMessage": "교사" + }, + "lib.translations.instance.users.roles.normal": { + "defaultMessage": "일반 사용자" + }, "lib.translations.messages.fetchingError": { "defaultMessage": "데이터를 로드하는 중 오류가 발생했습니다. 다시 시도하세요." }, @@ -8291,9 +8651,27 @@ "lib.translations.messages.loadImageError": { "defaultMessage": "이미지를 로드하는 중 오류가 발생했습니다. 다른 이미지를 선택해 보십시오." }, + "lib.translations.myStudents": { + "defaultMessage": "내 학생들" + }, + "lib.translations.myStudentsIncludingPhantoms": { + "defaultMessage": "내 학생들 (팬텀 포함)" + }, "lib.translations.no": { "defaultMessage": "아니오" }, + "lib.translations.staff": { + "defaultMessage": "스태프" + }, + "lib.translations.staffIncludingPhantoms": { + "defaultMessage": "스태프 (팬텀 포함)" + }, + "lib.translations.students": { + "defaultMessage": "학생들" + }, + "lib.translations.studentsIncludingPhantoms": { + "defaultMessage": "학생들 (팬텀 포함)" + }, "lib.translations.summary": { "defaultMessage": "요약" }, @@ -8330,15 +8708,15 @@ "lib.translations.table.column.createdAt": { "defaultMessage": "생성 시각" }, + "lib.translations.table.column.currentExternalId": { + "defaultMessage": "현재 외부 ID" + }, "lib.translations.table.column.designation": { "defaultMessage": "지정" }, "lib.translations.table.column.email": { "defaultMessage": "이메일" }, - "lib.translations.table.column.externalId": { - "defaultMessage": "외부 ID" - }, "lib.translations.table.column.endAt": { "defaultMessage": "종료 시각" }, @@ -8348,6 +8726,9 @@ "lib.translations.table.column.experiencePointsAwarded": { "defaultMessage": "획득한 경험치" }, + "lib.translations.table.column.externalId": { + "defaultMessage": "외부 ID" + }, "lib.translations.table.column.groups": { "defaultMessage": "그룹" }, @@ -8390,6 +8771,9 @@ "lib.translations.table.column.name": { "defaultMessage": "이름" }, + "lib.translations.table.column.newExternalId": { + "defaultMessage": "새 외부 ID" + }, "lib.translations.table.column.optional": { "defaultMessage": "선택" }, @@ -8471,30 +8855,15 @@ "material.attemptLoader.errorAccessingMaterial": { "defaultMessage": "이 자료에 접근하는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요." }, - "system.admin.instance.instance.InstanceAdminNavigator.announcements": { - "defaultMessage": "공지사항" - }, - "system.admin.instance.instance.InstanceAdminNavigator.components": { - "defaultMessage": "구성 요소" - }, - "system.admin.instance.instance.InstanceAdminNavigator.courses": { - "defaultMessage": "과정" - }, - "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { - "defaultMessage": "역할 요청" - }, - "system.admin.instance.instance.InstanceAdminNavigator.users": { - "defaultMessage": "사용자" - }, - "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { - "defaultMessage": "도움 받기" - }, "system.admin.admin.AdminNavigator.announcements": { "defaultMessage": "시스템 공지사항" }, "system.admin.admin.AdminNavigator.courses": { "defaultMessage": "과정" }, + "system.admin.admin.AdminNavigator.getHelp": { + "defaultMessage": "도움 받기" + }, "system.admin.admin.AdminNavigator.instances": { "defaultMessage": "인스턴스" }, @@ -8504,9 +8873,6 @@ "system.admin.admin.AdminNavigator.users": { "defaultMessage": "사용자" }, - "system.admin.admin.AdminNavigator.getHelp": { - "defaultMessage": "도움 받기" - }, "system.admin.admin.AnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "공지사항을 가져올 수 없습니다." }, @@ -8597,18 +8963,18 @@ "system.admin.admin.UsersButton.deletionConfirm": { "defaultMessage": "정말로 계속하시겠습니까?" }, - "system.admin.admin.UsersButton.deletionFailure": { - "defaultMessage": "사용자 삭제에 실패했습니다 - {error}" - }, - "system.admin.admin.UsersButton.deletionSuccess": { - "defaultMessage": "사용자가 삭제되었습니다." - }, "system.admin.admin.UsersButton.deletionConfirmTitle": { "defaultMessage": "{role} 사용자 {name} ({email}) 삭제 중" }, + "system.admin.admin.UsersButton.deletionFailure": { + "defaultMessage": "사용자 삭제에 실패했습니다 - {error}" + }, "system.admin.admin.UsersButton.deletionPromptContent": { "defaultMessage": "이 사용자를 삭제하면 다음 {count, plural, one {과목} other {과목들}}에 연결된 데이터가 영구적으로 삭제됩니다:" }, + "system.admin.admin.UsersButton.deletionSuccess": { + "defaultMessage": "사용자가 삭제되었습니다." + }, "system.admin.admin.UsersIndex.activeUsers": { "defaultMessage": "활성 사용자: {allCount} ({adminCount} 관리자, {normalCount} 일반){br}(지난 7일 동안 활성)" }, @@ -8624,9 +8990,6 @@ "system.admin.admin.UsersTable.changeRoleSuccess": { "defaultMessage": "{name}의 역할이 {role}(으)로 변경되었습니다." }, - "system.admin.users.UsersTable.instanceEntry": { - "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one { (1개 과목)} other { ({courseCount}개 과목)}}" - }, "system.admin.admin.UsersTable.renameSuccess": { "defaultMessage": "{oldName}이(가) {newName}(으)로 이름이 변경되었습니다." }, @@ -8651,8 +9014,26 @@ "system.admin.instance.instance.IndividualInvitations.appendNewRow": { "defaultMessage": "새 행 추가" }, - "system.admin.instance.instance.IndividualInvitations.invite": { - "defaultMessage": "모든 사용자 초대" + "system.admin.instance.instance.IndividualInvitations.invite": { + "defaultMessage": "모든 사용자 초대" + }, + "system.admin.instance.instance.InstanceAdminNavigator.announcements": { + "defaultMessage": "공지사항" + }, + "system.admin.instance.instance.InstanceAdminNavigator.components": { + "defaultMessage": "구성 요소" + }, + "system.admin.instance.instance.InstanceAdminNavigator.courses": { + "defaultMessage": "과정" + }, + "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { + "defaultMessage": "도움 받기" + }, + "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { + "defaultMessage": "역할 요청" + }, + "system.admin.instance.instance.InstanceAdminNavigator.users": { + "defaultMessage": "사용자" }, "system.admin.instance.instance.InstanceAnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "공지사항을 가져올 수 없습니다." @@ -8774,6 +9155,27 @@ "system.admin.instance.instance.InstanceUsersTabs.usersTab": { "defaultMessage": "사용자" }, + "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "{name} ({email})에 대한 초대를 삭제하시겠습니까?" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { + "defaultMessage": "사용자를 삭제하지 못했습니다 - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "{name}의 초대가 삭제되었습니다." + }, + "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "초대 삭제" + }, + "system.admin.instance.instance.InvitationActionButtons.resendFailure": { + "defaultMessage": "초대를 다시 보내지 못했습니다 - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { + "defaultMessage": "{email}로 초대 이메일을 다시 보냈습니다!" + }, + "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { + "defaultMessage": "초대 다시 보내기" + }, "system.admin.instance.instance.InvitationResultDialog.close": { "defaultMessage": "닫기" }, @@ -8804,27 +9206,6 @@ "system.admin.instance.instance.InvitationResultDialog.newInvitations": { "defaultMessage": "새 초대 ({count})" }, - "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "{name} ({email})에 대한 초대를 삭제하시겠습니까?" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { - "defaultMessage": "사용자를 삭제하지 못했습니다 - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "{name}의 초대가 삭제되었습니다." - }, - "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "초대 삭제" - }, - "system.admin.instance.instance.InvitationActionButtons.resendFailure": { - "defaultMessage": "초대를 다시 보내지 못했습니다 - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { - "defaultMessage": "{email}로 초대 이메일을 다시 보냈습니다!" - }, - "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { - "defaultMessage": "초대 다시 보내기" - }, "system.admin.instance.instance.PendingRoleRequestsButton.approveFailure": { "defaultMessage": "역할 요청을 승인하지 못했습니다 - {error}" }, @@ -8870,6 +9251,9 @@ "system.admin.instance.instance.UserInvitationsTable.accepted": { "defaultMessage": "수락됨" }, + "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "{confirmedAt}에 수락됨" + }, "system.admin.instance.instance.UserInvitationsTable.failed": { "defaultMessage": "실패" }, @@ -8882,27 +9266,24 @@ "system.admin.instance.instance.UserInvitationsTable.sentTooltip": { "defaultMessage": "{sentAt}에 전송됨" }, - "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "{confirmedAt}에 수락됨" - }, "system.admin.instance.instance.UsersButton.deleteTooltip": { "defaultMessage": "사용자 제거" }, "system.admin.instance.instance.UsersButton.deletionConfirm": { "defaultMessage": "정말로 계속하시겠습니까?" }, - "system.admin.instance.instance.UsersButton.deletionFailure": { - "defaultMessage": "사용자 제거에 실패했습니다 - {error}" - }, - "system.admin.instance.instance.UsersButton.deletionSuccess": { - "defaultMessage": "사용자가 이 인스턴스에서 제거되었습니다." - }, "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { "defaultMessage": "{role} 사용자 {name} ({email}) 제거 중" }, + "system.admin.instance.instance.UsersButton.deletionFailure": { + "defaultMessage": "사용자 제거에 실패했습니다 - {error}" + }, "system.admin.instance.instance.UsersButton.deletionPromptContent": { "defaultMessage": "이 사용자를 제거하면 다음 {count, plural, one {과목} other {과목들}}에서 오류가 발생할 수 있습니다:" }, + "system.admin.instance.instance.UsersButton.deletionSuccess": { + "defaultMessage": "사용자가 이 인스턴스에서 제거되었습니다." + }, "system.admin.instance.instance.UsersTable.changeRoleSuccess": { "defaultMessage": "{name}의 역할이 {role}(으)로 성공적으로 변경되었습니다." }, @@ -8924,6 +9305,9 @@ "system.admin.users.UsersTable.fetchFilteredUsersFailure": { "defaultMessage": "사용자를 가져오지 못했습니다." }, + "system.admin.users.UsersTable.instanceEntry": { + "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one { (1개 과목)} other { ({courseCount}개 과목)}}" + }, "user.accountSettings": { "defaultMessage": "계정 설정" }, @@ -9226,191 +9610,5 @@ }, "users.troubleSigningIn": { "defaultMessage": "로그인에 문제가 있으신가요?" - }, - "course.userInvitations.ExternalIdConflictPrompt.title": { - "defaultMessage": "외부 ID 업데이트 확인" - }, - "course.userInvitations.ExternalIdConflictPrompt.body": { - "defaultMessage": "이 사용자들은 이미 등록되었거나 초대가 대기 중입니다. 새로운 초대 이메일은 발송되지 않습니다. 현재 외부 ID를 유지하거나 파일의 값으로 교체하시겠습니까?" - }, - "course.userInvitations.ExternalIdConflictPrompt.pendingInvitationUpdates": { - "defaultMessage": "대기 중인 초대 업데이트 ({count}개)" - }, - "course.userInvitations.ExternalIdConflictPrompt.pendingCourseUserUpdates": { - "defaultMessage": "대기 중인 수강생 업데이트 ({count}개)" - }, - "course.userInvitations.ExternalIdConflictPrompt.goBack": { - "defaultMessage": "돌아가기" - }, - "course.userInvitations.ExternalIdConflictPrompt.keepExisting": { - "defaultMessage": "현재 유지" - }, - "course.userInvitations.ExternalIdConflictPrompt.replace": { - "defaultMessage": "교체" - }, - "lib.translations.table.column.currentExternalId": { - "defaultMessage": "현재 외부 ID" - }, - "lib.translations.table.column.newExternalId": { - "defaultMessage": "새 외부 ID" - }, - "course.admin.GradebookSettings.gradebookSettings": { - "defaultMessage": "성적부 설정" - }, - "course.admin.GradebookSettings.weightedViewEnabled": { - "defaultMessage": "가중 성적 보기 사용" - }, - "course.admin.GradebookSettings.weightedViewEnabledHint": { - "defaultMessage": "성적부에 \"가중 총점\" 보기를 추가하여 교직원이 탭별 가중치를 설정하고 가중 총점 열을 확인할 수 있습니다." - }, - "course.gradebook.ConfigureWeightsPrompt.allExcluded": { - "defaultMessage": "“{tab}”의 모든 평가가 제외되어 총점에 반영되지 않습니다." - }, - "course.gradebook.ConfigureWeightsPrompt.allExcludedCount": { - "defaultMessage": "전체 {n}개 제외됨" - }, - "course.gradebook.ConfigureWeightsPrompt.customMode": { - "defaultMessage": "사용자 지정" - }, - "course.gradebook.ConfigureWeightsPrompt.customSum": { - "defaultMessage": "평가 가중치: {sum} / {total}" - }, - "course.gradebook.ConfigureWeightsPrompt.defaultsHint": { - "defaultMessage": "아직 설정된 가중치가 없습니다. 모든 탭이 동일한 비중으로 반영되도록 제안된 기본값입니다. 저장하여 확정하거나 아래에서 조정하세요." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionDrop": { - "defaultMessage": "동일 모드에서는 평균을 내기 전에 각 학생의 점수가 가장 낮은 평가 N개를 선택적으로 제외할 수 있습니다." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionExclusion": { - "defaultMessage": "탭을 펼쳐 개별 평가를 성적에 포함하거나 제외하세요." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionIntro": { - "defaultMessage": "각 탭과 평가가 학생의 총 성적에 어떻게 반영되는지 관리합니다." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionModes": { - "defaultMessage": "“동일”(모든 평가가 탭의 가중치를 동일하게 나눔)또는 “사용자 지정”(각 평가의 비중을 설정)을 선택하세요." - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionWeights": { - "defaultMessage": "각 탭의 가중치, 즉 총 성적에 기여하는 비중을 설정하세요. 가중치 합계는 100이어야 합니다." - }, - "course.gradebook.ConfigureWeightsPrompt.equalMode": { - "defaultMessage": "동일" - }, - "course.gradebook.ConfigureWeightsPrompt.excluded": { - "defaultMessage": "제외됨" - }, - "course.gradebook.ConfigureWeightsPrompt.excludedCount": { - "defaultMessage": "{n}개 제외됨" - }, - "course.gradebook.ConfigureWeightsPrompt.includeAssessment": { - "defaultMessage": "{assessment} 성적에 포함" - }, - "course.gradebook.ConfigureWeightsPrompt.modeAria": { - "defaultMessage": "{tab} 가중치 모드" - }, - "course.gradebook.ConfigureWeightsPrompt.ofGrade": { - "defaultMessage": "성적의 {pct}%" - }, - "course.gradebook.ConfigureWeightsPrompt.promptTitle": { - "defaultMessage": "기여도 설정" - }, - "course.gradebook.ConfigureWeightsPrompt.saveError": { - "defaultMessage": "가중치를 저장하지 못했습니다. 다시 시도해 주세요." - }, - "course.gradebook.ConfigureWeightsPrompt.total": { - "defaultMessage": "합계: {sum}%" - }, - "course.gradebook.ConfigureWeightsPrompt.unbalanced": { - "defaultMessage": "저장하기 전에 “{tab}”의 평가 가중치 합계가 해당 탭의 총 가중치와 같아야 합니다." - }, - "course.gradebook.ConfigureWeightsPrompt.valueTooHigh": { - "defaultMessage": "값은 최대 100이어야 합니다" - }, - "course.gradebook.ConfigureWeightsPrompt.valueTooLow": { - "defaultMessage": "값은 최소 0이어야 합니다" - }, - "course.gradebook.ConfigureWeightsPrompt.weightsDoNotSum": { - "defaultMessage": "가중치 합계가 100이 아닙니다. 저장은 가능하지만 총점이 정확하지 않을 수 있습니다." - }, - "course.gradebook.GradebookIndex.allAssessments": { - "defaultMessage": "전체 평가" - }, - "course.gradebook.GradebookIndex.byWeight": { - "defaultMessage": "가중 총점" - }, - "course.gradebook.GradebookWeightedTable.collapseRow": { - "defaultMessage": "{name} 접기" - }, - "course.gradebook.GradebookWeightedTable.configureWeights": { - "defaultMessage": "가중치 설정" - }, - "course.gradebook.GradebookWeightedTable.defaultWeights": { - "defaultMessage": "기본 가중치를 표시하고 있습니다. 모든 탭이 동일하게 반영됩니다. \"가중치 설정\"을 클릭하여 직접 설정하세요." - }, - "course.gradebook.GradebookWeightedTable.defaultWeightsNoAccess": { - "defaultMessage": "기본 가중치를 표시하고 있습니다. 가중치가 설정되기 전까지 모든 탭이 동일하게 반영됩니다." - }, - "course.gradebook.GradebookWeightedTable.displayPercent": { - "defaultMessage": "백분율" - }, - "course.gradebook.GradebookWeightedTable.displayPercentTooltip": { - "defaultMessage": "학생이 각 탭에서 획득한 비율입니다. 비중이 20%인 탭에서 100%는 해당 탭의 20점을 모두 획득했음을 의미합니다." - }, - "course.gradebook.GradebookWeightedTable.displayPoints": { - "defaultMessage": "점수" - }, - "course.gradebook.GradebookWeightedTable.displayPointsTooltip": { - "defaultMessage": "각 탭이 기여하는 성적 점수입니다. 각 열의 합이 예상 총점이 됩니다." - }, - "course.gradebook.GradebookWeightedTable.downloadCsv": { - "defaultMessage": "CSV로 다운로드" - }, - "course.gradebook.GradebookWeightedTable.email": { - "defaultMessage": "이메일" - }, - "course.gradebook.GradebookWeightedTable.excluded": { - "defaultMessage": "제외됨" - }, - "course.gradebook.GradebookWeightedTable.expandRow": { - "defaultMessage": "{name} 펼치기" - }, - "course.gradebook.GradebookWeightedTable.name": { - "defaultMessage": "이름" - }, - "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { - "defaultMessage": "설정된 가중치가 없습니다. 모든 탭의 가중치가 0입니다. \"가중치 설정\"을 클릭하여 가중치를 지정하세요." - }, - "course.gradebook.GradebookWeightedTable.noWeightsNoAccess": { - "defaultMessage": "아직 탭 가중치가 설정되지 않았습니다." - }, - "course.gradebook.GradebookWeightedTable.outOfWeight": { - "defaultMessage": "/{weight}" - }, - "course.gradebook.GradebookWeightedTable.percentOfGrade": { - "defaultMessage": "성적의 {weight}%" - }, - "course.gradebook.GradebookWeightedTable.percentTotalExact": { - "defaultMessage": "합계 100%" - }, - "course.gradebook.GradebookWeightedTable.percentTotalWarning": { - "defaultMessage": "합계 {weight}%" - }, - "course.gradebook.GradebookWeightedTable.total": { - "defaultMessage": "총점" - }, - "course.gradebook.GradebookWeightedTable.searchStudents": { - "defaultMessage": "학생 검색" - }, - "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { - "defaultMessage": "가중치 합계가 100이 아닙니다. 총점이 정확하지 않을 수 있습니다." - }, - "course.gradebook.TotalHint.policy": { - "defaultMessage": "총점은 채점되지 않은 평가를 0점으로 계산합니다." - }, - "course.gradebook.WeightedViewHint.hint": { - "defaultMessage": "가중 총점이 필요하신가요? 각 탭이 학생의 전체 성적에 반영되는 비중을 설정하고 여기에서 가중 총점을 확인할 수 있습니다. {link}에서 활성화하세요." - }, - "course.gradebook.WeightedViewHint.settingsLink": { - "defaultMessage": "성적부 설정" } } diff --git a/client/locales/zh.json b/client/locales/zh.json index 9d5c753e3e..5e6f49cfc9 100644 --- a/client/locales/zh.json +++ b/client/locales/zh.json @@ -29,6 +29,9 @@ "app.DashboardPage.yourCourses": { "defaultMessage": "你的课程" }, + "app.ErrorPage.courseSuspended": { + "defaultMessage": "此课程已暂停。" + }, "app.ErrorPage.error": { "defaultMessage": "咔嘣,一颗流星刚刚坠毁" }, @@ -59,14 +62,11 @@ "app.ErrorPage.notFoundSubtitle": { "defaultMessage": "检查你输入的地址是否正确,请重试或者返回主页。" }, - "app.ErrorPage.userSuspended": { - "defaultMessage": "您访问此课程的权限已被暂停。" - }, "app.ErrorPage.suspendedSubtitle": { "defaultMessage": "请联系您的讲师或课程工作人员。" }, - "app.ErrorPage.courseSuspended": { - "defaultMessage": "此课程已暂停。" + "app.ErrorPage.userSuspended": { + "defaultMessage": "您访问此课程的权限已被暂停。" }, "app.Footer.contactUs": { "defaultMessage": "联系我们" @@ -80,12 +80,12 @@ "app.Footer.instructorsGuide": { "defaultMessage": "讲师使用指南" }, - "app.Footer.reportIssue": { - "defaultMessage": "报告问题" - }, "app.Footer.privacyPolicy": { "defaultMessage": "隐私政策" }, + "app.Footer.reportIssue": { + "defaultMessage": "报告问题" + }, "app.Footer.termsOfService": { "defaultMessage": "服务条款" }, @@ -263,23 +263,23 @@ "course.achievement.AchievementAward.AchievementAwardManager.saveChanges": { "defaultMessage": "保存更改" }, + "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { + "defaultMessage": "已授予学生" + }, "course.achievement.AchievementAward.AchievementAwardSummary.name": { "defaultMessage": "姓名" }, - "course.achievement.AchievementAward.AchievementAwardSummary.userType": { - "defaultMessage": "用户类型" + "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { + "defaultMessage": "普通学生" }, - "course.achievement.AchievementAward.AchievementAwardSummary.awardedStudents": { - "defaultMessage": "已授予学生" + "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { + "defaultMessage": "旁听生" }, "course.achievement.AchievementAward.AchievementAwardSummary.revokedStudents": { "defaultMessage": "已撤销学生" }, - "course.achievement.AchievementAward.AchievementAwardSummary.phantomStudent": { - "defaultMessage": "旁听生" - }, - "course.achievement.AchievementAward.AchievementAwardSummary.normalStudent": { - "defaultMessage": "普通学生" + "course.achievement.AchievementAward.AchievementAwardSummary.userType": { + "defaultMessage": "用户类型" }, "course.achievement.AchievementAward.awardAchievement": { "defaultMessage": "获奖成就" @@ -353,26 +353,26 @@ "course.achievement.AchievementShow.studentsWithAchievement": { "defaultMessage": "有此成就的学生" }, - "course.achievement.AchievementTable.noAchievement": { - "defaultMessage": "暂无成就" + "course.achievement.AchievementTable.actions": { + "defaultMessage": "操作" }, "course.achievement.AchievementTable.badge": { "defaultMessage": "徽章" }, - "course.achievement.AchievementTable.title": { - "defaultMessage": "标题" - }, "course.achievement.AchievementTable.description": { "defaultMessage": "描述" }, - "course.achievement.AchievementTable.requirements": { - "defaultMessage": "要求" + "course.achievement.AchievementTable.noAchievement": { + "defaultMessage": "暂无成就" }, "course.achievement.AchievementTable.published": { "defaultMessage": "已发布" }, - "course.achievement.AchievementTable.actions": { - "defaultMessage": "操作" + "course.achievement.AchievementTable.requirements": { + "defaultMessage": "要求" + }, + "course.achievement.AchievementTable.title": { + "defaultMessage": "标题" }, "course.achievement.AchievementsIndex.achievements": { "defaultMessage": "成就" @@ -515,48 +515,9 @@ "course.admin.AssessmentSettings.toTab": { "defaultMessage": "到 {tab}" }, - "course.admin.CodaveriSettings.codaveriModel": { - "defaultMessage": "模型" - }, - "course.admin.CodaveriSettings.codaveriModelDescription": { - "defaultMessage": "Codaveri 使用的 AI 模型,用于为学生的编程问题生成帮助对话。" - }, - "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { - "defaultMessage": "您可以在此提供指令以自定义 Codaveri 模型的行为。{br} 在帮助学生时,这些指令将与您在具体题目中设置的指令一并执行。{br}要引用题目相关的细节,您可以在提示中使用以下变量,并按照如下方式用括号写出:" - }, - "course.admin.CodaveriSettings.codaveriSystemPromptProblemDescriptionLine": { - "defaultMessage": "{problemDescriptionVar} :编程问题的完整描述。" - }, - "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { - "defaultMessage": "{studentFilePathsVar} :学生正在处理的文件路径(以逗号分隔)。" - }, - "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { - "defaultMessage": "目前这是一项实验性功能。 Codaveri 为学生的代码提供代码测验和自动代码反馈服务。" - }, - "course.admin.CodaveriSettings.codaveriSettings": { - "defaultMessage": "Codaveri 设置" - }, - "course.admin.CodaveriSettings.error": { - "defaultMessage": "更新 codaveri 设置时出错。" - }, "course.admin.CodaveriSettings.Some": { "defaultMessage": "一些" }, - "course.admin.CodaveriSettings.feedbackWorkflow": { - "defaultMessage": "自动提交后评论" - }, - "course.admin.CodaveriSettings.feedbackWorkflowDescription": { - "defaultMessage": "当包含编程题目的提交被最终提交时," - }, - "course.admin.CodaveriSettings.feedbackWorkflowNone": { - "defaultMessage": "不生成反馈" - }, - "course.admin.CodaveriSettings.feedbackWorkflowDraft": { - "defaultMessage": "生成草稿反馈,需要工作人员批准" - }, - "course.admin.CodaveriSettings.feedbackWorkflowPublish": { - "defaultMessage": "直接向学生发布反馈" - }, "course.admin.CodaveriSettings.assessments": { "defaultMessage": "评估" }, @@ -569,32 +530,83 @@ "course.admin.CodaveriSettings.codaveriEngineDescription": { "defaultMessage": "用于生成编程代码反馈的 Codaveri 引擎类型" }, + "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { + "defaultMessage": "Codaveri 评估器" + }, + "course.admin.CodaveriSettings.codaveriModel": { + "defaultMessage": "模型" + }, + "course.admin.CodaveriSettings.codaveriModelDescription": { + "defaultMessage": "Codaveri 使用的 AI 模型,用于为学生的编程问题生成帮助对话。" + }, "course.admin.CodaveriSettings.codaveriOverrideSystemPrompt": { "defaultMessage": "使用自定义系统提示" }, "course.admin.CodaveriSettings.codaveriOverrideSystemPromptDescription": { "defaultMessage": "在帮助学生时,这些指令将与您在题目本身设置的任何指令一起执行。要引用题目特定的详细信息,您可以在提示中使用这些变量,并按照如下所示用括号写出:" }, + "course.admin.CodaveriSettings.codaveriSettings": { + "defaultMessage": "Codaveri 设置" + }, + "course.admin.CodaveriSettings.codaveriSettingsSubtitle": { + "defaultMessage": "目前这是一项实验性功能。 Codaveri 为学生的代码提供代码测验和自动代码反馈服务。" + }, "course.admin.CodaveriSettings.codaveriSystemPrompt": { "defaultMessage": "系统提示" }, + "course.admin.CodaveriSettings.codaveriSystemPromptDescription": { + "defaultMessage": "您可以在此提供指令以自定义 Codaveri 模型的行为。{br} 在帮助学生时,这些指令将与您在具体题目中设置的指令一并执行。{br}要引用题目相关的细节,您可以在提示中使用以下变量,并按照如下方式用括号写出:" + }, + "course.admin.CodaveriSettings.codaveriSystemPromptProblemDescriptionLine": { + "defaultMessage": "{problemDescriptionVar} :编程问题的完整描述。" + }, + "course.admin.CodaveriSettings.codaveriSystemPromptStudentFilePathsLine": { + "defaultMessage": "{studentFilePathsVar} :学生正在处理的文件路径(以逗号分隔)。" + }, "course.admin.CodaveriSettings.codaveriUseDefaultSystemPrompt": { "defaultMessage": "使用默认系统提示" }, + "course.admin.CodaveriSettings.enableDisableButton": { + "defaultMessage": "{enabled, select, true {启用} other {禁用}}" + }, + "course.admin.CodaveriSettings.enableDisableEvaluator": { + "defaultMessage": "{enabled, select, true {启用} other {禁用}} Codaveri 评估器 对 {questionCount} 道编程题目在 {title} 中?" + }, + "course.admin.CodaveriSettings.enableDisableEvaluatorDescription": { + "defaultMessage": "这 {type} 中的 {questionCount} 道编程题目将使用 {enabled, select, true {Codaveri} other {默认}} 评估器" + }, + "course.admin.CodaveriSettings.enableDisableLiveFeedback": { + "defaultMessage": "{enabled, select, true {启用} other {禁用}} 实时帮助 对 {questionCount} 道编程题目在 {title} 中?" + }, + "course.admin.CodaveriSettings.error": { + "defaultMessage": "更新 codaveri 设置时出错。" + }, + "course.admin.CodaveriSettings.errorOccurredWhenUpdatingCodaveriEvaluatorSettings": { + "defaultMessage": "更新 Codaveri 评估器设置时发生了错误。" + }, + "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { + "defaultMessage": "更新实时帮助设置时发生了错误。" + }, "course.admin.CodaveriSettings.evaluatorUpdateSuccess": { "defaultMessage": "{question} 现在使用 {evaluator} 评估器" }, "course.admin.CodaveriSettings.expandAll": { "defaultMessage": "展开所有问题" }, - "course.admin.CodaveriSettings.programmingQuestionSettings": { - "defaultMessage": "编程题目设置" + "course.admin.CodaveriSettings.feedbackWorkflow": { + "defaultMessage": "自动提交后评论" }, - "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { - "defaultMessage": "为各种评估中的编程题目启用/禁用 Codaveri 作为评估器。" + "course.admin.CodaveriSettings.feedbackWorkflowDescription": { + "defaultMessage": "当包含编程题目的提交被最终提交时," }, - "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { - "defaultMessage": "成功更新所有问题以使用 {evaluator} 评估器" + "course.admin.CodaveriSettings.feedbackWorkflowDraft": { + "defaultMessage": "生成草稿反馈,需要工作人员批准" + }, + "course.admin.CodaveriSettings.feedbackWorkflowNone": { + "defaultMessage": "不生成反馈" + }, + "course.admin.CodaveriSettings.feedbackWorkflowPublish": { + "defaultMessage": "直接向学生发布反馈" }, "course.admin.CodaveriSettings.getHelpUsageLimit": { "defaultMessage": "限制每位学生的获取帮助消息数" @@ -602,35 +614,23 @@ "course.admin.CodaveriSettings.getHelpUsageLimitDescription": { "defaultMessage": "如果启用,学生将只能发送有限数量的消息。学生将能够看到此限制以及剩余的消息数量。" }, - "course.admin.CodaveriSettings.maxGetHelpUserMessages": { - "defaultMessage": "每个问题的最大消息数" - }, - "course.admin.CodaveriSettings.errorOccurredWhenUpdatingCodaveriEvaluatorSettings": { - "defaultMessage": "更新 Codaveri 评估器设置时发生了错误。" - }, - "course.admin.CodaveriSettings.codaveriEvaluatorSettings": { - "defaultMessage": "Codaveri 评估器" + "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { + "defaultMessage": "对于 {question},实时帮助现在已 {liveFeedbackEnabled, select, true {启用} other {禁用}}" }, "course.admin.CodaveriSettings.liveFeedbackSettings": { "defaultMessage": "实时帮助" }, - "course.admin.CodaveriSettings.errorOccurredWhenUpdatingLiveFeedbackSettings": { - "defaultMessage": "更新实时帮助设置时发生了错误。" - }, - "course.admin.CodaveriSettings.enableDisableButton": { - "defaultMessage": "{enabled, select, true {启用} other {禁用}}" - }, - "course.admin.CodaveriSettings.enableDisableEvaluator": { - "defaultMessage": "{enabled, select, true {启用} other {禁用}} Codaveri 评估器 对 {questionCount} 道编程题目在 {title} 中?" + "course.admin.CodaveriSettings.maxGetHelpUserMessages": { + "defaultMessage": "每个问题的最大消息数" }, - "course.admin.CodaveriSettings.enableDisableLiveFeedback": { - "defaultMessage": "{enabled, select, true {启用} other {禁用}} 实时帮助 对 {questionCount} 道编程题目在 {title} 中?" + "course.admin.CodaveriSettings.programmingQuestionSettings": { + "defaultMessage": "编程题目设置" }, - "course.admin.CodaveriSettings.enableDisableEvaluatorDescription": { - "defaultMessage": "这 {type} 中的 {questionCount} 道编程题目将使用 {enabled, select, true {Codaveri} other {默认}} 评估器" + "course.admin.CodaveriSettings.programmingQuestionSettingsSubtitle": { + "defaultMessage": "为各种评估中的编程题目启用/禁用 Codaveri 作为评估器。" }, - "course.admin.CodaveriSettings.liveFeedbackEnabledUpdateSuccess": { - "defaultMessage": "对于 {question},实时帮助现在已 {liveFeedbackEnabled, select, true {启用} other {禁用}}" + "course.admin.CodaveriSettings.succesfulUpdateAllEvaluator": { + "defaultMessage": "成功更新所有问题以使用 {evaluator} 评估器" }, "course.admin.CodaveriSettings.successfulUpdateAllLiveFeedbackEnabled": { "defaultMessage": "成功地将所有问题的实时帮助设置为 {liveFeedbackEnabled, select, true {启用} other {禁用}}" @@ -677,6 +677,12 @@ "course.admin.CourseSettings.courseSettings": { "defaultMessage": "课程设置" }, + "course.admin.CourseSettings.courseSuspensionMessage": { + "defaultMessage": "课程暂停消息" + }, + "course.admin.CourseSettings.courseSuspensionMessageDescription": { + "defaultMessage": "此消息将在课程暂停期间显示给用户。留空以显示默认消息。" + }, "course.admin.CourseSettings.daysInAdvance": { "defaultMessage": "提前几天" }, @@ -764,23 +770,14 @@ "course.admin.CourseSettings.stragglersDescription": { "defaultMessage": "不让任何人掉队;如果学生延迟完成测验,随后的关闭参考时间将被推迟。" }, - "course.admin.CourseSettings.suspension": { - "defaultMessage": "访问暂停" - }, "course.admin.CourseSettings.suspendCourse": { "defaultMessage": "暂停课程" }, "course.admin.CourseSettings.suspendCourseDescription": { "defaultMessage": "被暂停的课程对所有学生不可访问。讲师仍可访问课程,所有学生数据将被保留。" }, - "course.admin.CourseSettings.unsuspendCourse": { - "defaultMessage": "解除课程暂停" - }, - "course.admin.CourseSettings.courseSuspensionMessage": { - "defaultMessage": "课程暂停消息" - }, - "course.admin.CourseSettings.courseSuspensionMessageDescription": { - "defaultMessage": "此消息将在课程暂停期间显示给用户。留空以显示默认消息。" + "course.admin.CourseSettings.suspendCourseFailure": { + "defaultMessage": "暂停课程时发生错误。" }, "course.admin.CourseSettings.suspendCoursePromptText": { "defaultMessage": "确定要暂停此课程吗?所有学生将无法访问,直到暂停被解除。" @@ -788,20 +785,8 @@ "course.admin.CourseSettings.suspendCourseSuccess": { "defaultMessage": "此课程已被暂停。" }, - "course.admin.CourseSettings.suspendCourseFailure": { - "defaultMessage": "暂停课程时发生错误。" - }, - "course.admin.CourseSettings.unsuspendCourseSuccess": { - "defaultMessage": "此课程的暂停已解除。" - }, - "course.admin.CourseSettings.unsuspendCourseFailure": { - "defaultMessage": "解除课程暂停时发生错误。" - }, - "course.admin.CourseSettings.userSuspensionMessage": { - "defaultMessage": "用户暂停消息" - }, - "course.admin.CourseSettings.userSuspensionMessageDescription": { - "defaultMessage": "此消息将显示给访问此课程的权限已被暂停的个别用户。留空以显示默认消息。" + "course.admin.CourseSettings.suspension": { + "defaultMessage": "访问暂停" }, "course.admin.CourseSettings.timeSettings": { "defaultMessage": "时间设置" @@ -812,12 +797,27 @@ "course.admin.CourseSettings.titleRequired": { "defaultMessage": "课程名称必填。" }, + "course.admin.CourseSettings.unsuspendCourse": { + "defaultMessage": "解除课程暂停" + }, + "course.admin.CourseSettings.unsuspendCourseFailure": { + "defaultMessage": "解除课程暂停时发生错误。" + }, + "course.admin.CourseSettings.unsuspendCourseSuccess": { + "defaultMessage": "此课程的暂停已解除。" + }, "course.admin.CourseSettings.uploadANewImage": { "defaultMessage": "选择新图片" }, "course.admin.CourseSettings.uploadingLogo": { "defaultMessage": "正在上传你的新Logo..." }, + "course.admin.CourseSettings.userSuspensionMessage": { + "defaultMessage": "用户暂停消息" + }, + "course.admin.CourseSettings.userSuspensionMessageDescription": { + "defaultMessage": "此消息将显示给访问此课程的权限已被暂停的个别用户。留空以显示默认消息。" + }, "course.admin.CourseSettingst.confirmDeletePlaceholder": { "defaultMessage": "这是你撤消的最后机会!" }, @@ -872,6 +872,15 @@ "course.admin.ForumsSettings.markPostAsAnswerSetting": { "defaultMessage": "可以将帖子标作答案的用户" }, + "course.admin.GradebookSettings.gradebookSettings": { + "defaultMessage": "成绩册设置" + }, + "course.admin.GradebookSettings.weightedViewEnabled": { + "defaultMessage": "启用加权成绩视图" + }, + "course.admin.GradebookSettings.weightedViewEnabledHint": { + "defaultMessage": "在成绩册中启用“加权总成绩”视图,教职员可以配置各标签页的权重并查看加权总成绩列。" + }, "course.admin.LeaderboardSettings.displayUserCount": { "defaultMessage": "显示用户数" }, @@ -941,6 +950,9 @@ "course.admin.MaterialSettings.materialsSettings": { "defaultMessage": "资料设置" }, + "course.admin.NotificationSettings.component": { + "defaultMessage": "组件" + }, "course.admin.NotificationSettings.description": { "defaultMessage": "描述" }, @@ -1388,12 +1400,12 @@ "course.assessment.AssessmentForm.showMcqMrqSolution": { "defaultMessage": "显示 单选题/多选题 解决方案" }, - "course.assessment.AssessmentForm.showRubricToStudents": { - "defaultMessage": "向学生显示评分细则" - }, "course.assessment.AssessmentForm.showPrivate": { "defaultMessage": "显示私有测试用例" }, + "course.assessment.AssessmentForm.showRubricToStudents": { + "defaultMessage": "向学生显示评分细则" + }, "course.assessment.AssessmentForm.singlePage": { "defaultMessage": "单页" }, @@ -1496,8 +1508,23 @@ "course.assessment.edit.update": { "defaultMessage": "保存" }, - "course.assessment.generation.confirmDeleteConversation": { - "defaultMessage": "您确定要删除 \"{title}\" 及其所有历史记录吗?此操作不可撤销!" + "course.assessment.generation.allFieldsLocked": { + "defaultMessage": "所有字段都已锁定,无法生成内容。" + }, + "course.assessment.generation.confirmDeleteConversation": { + "defaultMessage": "您确定要删除 \"{title}\" 及其所有历史记录吗?此操作不可撤销!" + }, + "course.assessment.generation.createMode": { + "defaultMessage": "新建" + }, + "course.assessment.generation.createModeTooltip": { + "defaultMessage": "从头生成新的问题" + }, + "course.assessment.generation.enhanceMode": { + "defaultMessage": "增强" + }, + "course.assessment.generation.enhanceModeTooltip": { + "defaultMessage": "在当前问题的基础上进行构建" }, "course.assessment.generation.exportAction": { "defaultMessage": "导出" @@ -1508,89 +1535,77 @@ "course.assessment.generation.exportError": { "defaultMessage": "导出此问题时发生错误:{error}" }, - "course.assessment.generation.lockTooltip": { - "defaultMessage": "锁定以防止更改此部分" + "course.assessment.generation.generateError": { + "defaultMessage": "生成题目 {title} 时发生错误。" }, - "course.assessment.generation.newTab": { - "defaultMessage": "新建" + "course.assessment.generation.generateMcqPage": { + "defaultMessage": "生成单选题" }, - "course.assessment.generation.openExportDialog": { - "defaultMessage": "导出" + "course.assessment.generation.generateMrqPage": { + "defaultMessage": "生成多选题" }, - "course.assessment.generation.resetConversation": { - "defaultMessage": "重置" + "course.assessment.generation.generateMultipleSuccess": { + "defaultMessage": "成功生成了 {count} 道题目!" }, - "course.assessment.generation.unlockTooltip": { - "defaultMessage": "解锁以继续编辑此部分" + "course.assessment.generation.generateQuestion": { + "defaultMessage": "生成" }, - "course.assessment.generation.mrq.numberOfQuestionsField": { - "defaultMessage": "题目数量" + "course.assessment.generation.generateSuccess": { + "defaultMessage": "{title} 生成成功。" }, - "course.assessment.generation.promptPlaceholder": { - "defaultMessage": "请输入内容..." + "course.assessment.generation.loadingSourceError": { + "defaultMessage": "无法加载原始题目信息。" }, - "course.assessment.generation.generateQuestion": { - "defaultMessage": "生成" + "course.assessment.generation.lockTooltip": { + "defaultMessage": "锁定以防止更改此部分" }, - "course.assessment.generation.showInactive": { - "defaultMessage": "显示未启用项" + "course.assessment.generation.mrq.exportDialogHeader": { + "defaultMessage": "导出题目(已选择 {exportCount} 项)" + }, + "course.assessment.generation.mrq.numberOfQuestionsField": { + "defaultMessage": "题目数量" }, "course.assessment.generation.mrq.numberOfQuestionsRange": { "defaultMessage": "请输入 {min} 到 {max} 之间的数字" }, - "course.assessment.generation.enhanceMode": { - "defaultMessage": "增强" - }, - "course.assessment.generation.createMode": { + "course.assessment.generation.newTab": { "defaultMessage": "新建" }, - "course.assessment.generation.enhanceModeTooltip": { - "defaultMessage": "在当前问题的基础上进行构建" - }, - "course.assessment.generation.createModeTooltip": { - "defaultMessage": "从头生成新的问题" + "course.assessment.generation.openExportDialog": { + "defaultMessage": "导出" }, - "course.assessment.generation.mrq.exportDialogHeader": { - "defaultMessage": "导出题目(已选择 {exportCount} 项)" + "course.assessment.generation.promptPlaceholder": { + "defaultMessage": "请输入内容..." }, "course.assessment.generation.requireNonEmptyOptionError": { "defaultMessage": "题目必须至少包含一个非空选项" }, - "course.assessment.generation.untitledQuestion": { - "defaultMessage": "无标题题目" - }, - "course.assessment.question.multipleResponses.showOptions": { - "defaultMessage": "显示选项" - }, - "course.assessment.question.multipleResponses.hideOptions": { - "defaultMessage": "隐藏选项" - }, - "course.assessment.question.multipleResponses.noOptions": { - "defaultMessage": "无选项" + "course.assessment.generation.resetConversation": { + "defaultMessage": "重置" }, - "course.assessment.question.multipleResponses.title": { - "defaultMessage": "标题" + "course.assessment.generation.showInactive": { + "defaultMessage": "显示未启用项" }, - "course.assessment.generation.generateMrqPage": { - "defaultMessage": "生成多选题" + "course.assessment.generation.unlockTooltip": { + "defaultMessage": "解锁以继续编辑此部分" }, - "course.assessment.generation.generateMcqPage": { - "defaultMessage": "生成单选题" + "course.assessment.generation.untitledQuestion": { + "defaultMessage": "无标题题目" }, - "course.assessment.generation.generateMultipleSuccess": { - "defaultMessage": "成功生成了 {count} 道题目!" + "course.assessment.liveFeedback.comments": { + "defaultMessage": "评论" }, - "course.assessment.generation.generateSuccess": { - "defaultMessage": "{title} 生成成功。" + "course.assessment.liveFeedback.lineHeader": { + "defaultMessage": "第 {lineNumber} 行" }, - "course.assessment.generation.generateError": { - "defaultMessage": "生成题目 {title} 时发生错误。" + "course.assessment.liveFeedback.liveFeedbackName": { + "defaultMessage": "实时反馈" }, - "course.assessment.generation.loadingSourceError": { - "defaultMessage": "无法加载原始题目信息。" + "course.assessment.liveFeedback.messageTimingTitle": { + "defaultMessage": "生成时间:{usedAt}" }, - "course.assessment.generation.allFieldsLocked": { - "defaultMessage": "所有字段都已锁定,无法生成内容。" + "course.assessment.liveFeedback.questionTitle": { + "defaultMessage": "题目 {index}" }, "course.assessment.monitoring.alivePresenceHint": { "defaultMessage": "及时收到最后一次心跳" @@ -1673,6 +1688,54 @@ "course.assessment.newAssessment": { "defaultMessage": "新测验" }, + "course.assessment.plagiarism.actions": { + "defaultMessage": "操作" + }, + "course.assessment.plagiarism.baseSubmission": { + "defaultMessage": "基准提交" + }, + "course.assessment.plagiarism.comparedSubmission": { + "defaultMessage": "比较提交" + }, + "course.assessment.plagiarism.confirmStartMessage": { + "defaultMessage": "运行新的抄袭检查将删除之前的结果。" + }, + "course.assessment.plagiarism.confirmStartTitle": { + "defaultMessage": "确认抄袭检查?" + }, + "course.assessment.plagiarism.downloadPdf": { + "defaultMessage": "下载 PDF" + }, + "course.assessment.plagiarism.lastRunTime": { + "defaultMessage": "上次运行时间:{date}" + }, + "course.assessment.plagiarism.notStarted": { + "defaultMessage": "尚未运行抄袭检查" + }, + "course.assessment.plagiarism.plagiarism": { + "defaultMessage": "抄袭结果" + }, + "course.assessment.plagiarism.results": { + "defaultMessage": "抄袭结果(提交之间的相似度)" + }, + "course.assessment.plagiarism.searchByStudentName": { + "defaultMessage": "按学生姓名搜索" + }, + "course.assessment.plagiarism.showSelfPlagiarism": { + "defaultMessage": "包含自我抄袭比较(同一学生,不同课程)" + }, + "course.assessment.plagiarism.similarityScore": { + "defaultMessage": "相似性分数" + }, + "course.assessment.plagiarism.start": { + "defaultMessage": "新的抄袭检查" + }, + "course.assessment.plagiarism.status": { + "defaultMessage": "抄袭检查状态" + }, + "course.assessment.plagiarism.viewReport": { + "defaultMessage": "查看报告" + }, "course.assessment.question.forumPostResponses.enableTextResponse": { "defaultMessage": "包括一个文本字段,供学生进一步输入信息" }, @@ -1745,6 +1808,9 @@ "course.assessment.question.multipleResponses.grading": { "defaultMessage": "评分" }, + "course.assessment.question.multipleResponses.hideOptions": { + "defaultMessage": "隐藏选项" + }, "course.assessment.question.multipleResponses.ignoresRandomization": { "defaultMessage": "忽略随机" }, @@ -1781,6 +1847,9 @@ "course.assessment.question.multipleResponses.newResponseCannotUndo": { "defaultMessage": "这是一个新选项。如果在保存前删除,它会立即消失。" }, + "course.assessment.question.multipleResponses.noOptions": { + "defaultMessage": "无选项" + }, "course.assessment.question.multipleResponses.noSkillsCanCreateSkills": { "defaultMessage": "本课程中还没有设置技能,你可以在 技能 页面中创建。" }, @@ -1817,6 +1886,9 @@ "course.assessment.question.multipleResponses.saveChangesFirstBeforeConvertingMcqMrq": { "defaultMessage": "尝试转换此问题类型前,请先保存更改。" }, + "course.assessment.question.multipleResponses.showOptions": { + "defaultMessage": "显示选项" + }, "course.assessment.question.multipleResponses.skills": { "defaultMessage": "技能" }, @@ -1829,6 +1901,9 @@ "course.assessment.question.multipleResponses.staffOnlyCommentsHint": { "defaultMessage": "适用于内部笔记或文档,对学生不可见。" }, + "course.assessment.question.multipleResponses.title": { + "defaultMessage": "标题" + }, "course.assessment.question.multipleResponses.undoDeleteChoice": { "defaultMessage": "撤销删除选择" }, @@ -1904,12 +1979,12 @@ "course.assessment.question.programming.defaultEvaluator": { "defaultMessage": "默认" }, - "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { - "defaultMessage": "{name}: 已安装的依赖项" - }, "course.assessment.question.programming.defaultEvaluatorDependencyDescription": { "defaultMessage": "提交的代码将在一个容器化环境中运行,并且以下依赖项已本地安装。{br}如果您的编程问题需要未列出的依赖项,请联系我们,我们将考虑添加它。" }, + "course.assessment.question.programming.defaultEvaluatorDependencyTitle": { + "defaultMessage": "{name}: 已安装的依赖项" + }, "course.assessment.question.programming.defaultEvaluatorHint": { "defaultMessage": "无额外操作,只需按照下面的包运行代码,并报告测试结果即可。" }, @@ -2042,24 +2117,24 @@ "course.assessment.question.programming.packageCreationModeHint": { "defaultMessage": "一旦成功创建此问题,无法更改此模式,请谨慎选择。" }, - "course.assessment.question.programming.packageImportSuccess": { - "defaultMessage": "包已被正确引入。" + "course.assessment.question.programming.packageImportEvaluationError": { + "defaultMessage": "评估您的解决方案时发生错误。请仔细检查测试用例并重试。" + }, + "course.assessment.question.programming.packageImportEvaluationTimeout": { + "defaultMessage": "在规定时间内未收到评估器的响应。这可能表示所有评估器当前都很忙,请稍后再试。" + }, + "course.assessment.question.programming.packageImportGenericError": { + "defaultMessage": "无法导入软件包:{error}" }, "course.assessment.question.programming.packageImportInvalidPackage": { "defaultMessage": "无法导入软件包:上传的软件包结构无效。" }, - "course.assessment.question.programming.packageImportEvaluationTimeout": { - "defaultMessage": "在规定时间内未收到评估器的响应。这可能表示所有评估器当前都很忙,请稍后再试。" + "course.assessment.question.programming.packageImportSuccess": { + "defaultMessage": "包已被正确引入。" }, "course.assessment.question.programming.packageImportTimeLimitExceeded": { "defaultMessage": "解决方案未能在规定时间内完成测试用例的评估。" }, - "course.assessment.question.programming.packageImportEvaluationError": { - "defaultMessage": "评估您的解决方案时发生错误。请仔细检查测试用例并重试。" - }, - "course.assessment.question.programming.packageImportGenericError": { - "defaultMessage": "无法导入软件包:{error}" - }, "course.assessment.question.programming.packageInfoOnline": { "defaultMessage": "生成评估包" }, @@ -2162,155 +2237,197 @@ "course.assessment.question.programminquestion.questionSavedRedirecting": { "defaultMessage": "问题已保存" }, - "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { - "defaultMessage": "不能留空。" + "course.assessment.question.rubricPlayground.add": { + "defaultMessage": "添加" }, - "course.assessment.question.scribing.ScribingQuestionForm.chooseFileButton": { - "defaultMessage": "选择文件" + "course.assessment.question.rubricPlayground.addExistingAnswers": { + "defaultMessage": "添加现有答案" }, - "course.assessment.question.scribing.ScribingQuestionForm.descriptionFieldLabel": { - "defaultMessage": "描述" + "course.assessment.question.rubricPlayground.addNewCategory": { + "defaultMessage": "添加新类别" }, - "course.assessment.question.scribing.ScribingQuestionForm.fetchFailureMessage": { - "defaultMessage": "发生错误,请重试。" + "course.assessment.question.rubricPlayground.addNewGrade": { + "defaultMessage": "添加新分数" }, - "course.assessment.question.scribing.ScribingQuestionForm.fileAttachmentRequired": { - "defaultMessage": "需要文件附件。" + "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { + "defaultMessage": "添加 {inputComponent} 个随机学生答案" }, - "course.assessment.question.scribing.ScribingQuestionForm.fileUploaded": { - "defaultMessage": "已上传文件:" + "course.assessment.question.rubricPlayground.addSampleAnswers": { + "defaultMessage": "添加示例答案" }, - "course.assessment.question.scribing.ScribingQuestionForm.lessThanEqualZeroValidationError": { - "defaultMessage": "值必须大于 0。" + "course.assessment.question.rubricPlayground.addSampleAnswersTitle": { + "defaultMessage": "添加示例答案" }, - "course.assessment.question.scribing.ScribingQuestionForm.maximumGradeFieldLabel": { - "defaultMessage": "最高等级" + "course.assessment.question.rubricPlayground.answer": { + "defaultMessage": "答案" }, - "course.assessment.question.scribing.ScribingQuestionForm.noFileChosenMessage": { - "defaultMessage": "没有选中任何文件" + "course.assessment.question.rubricPlayground.apply": { + "defaultMessage": "应用" }, - "course.assessment.question.scribing.ScribingQuestionForm.positiveNumberValidationError": { - "defaultMessage": "必须为正值。" + "course.assessment.question.rubricPlayground.applyFailure": { + "defaultMessage": "应用评分结果失败" }, - "course.assessment.question.scribing.ScribingQuestionForm.resolveErrorsMessage": { - "defaultMessage": "此表单有错误,请在提交前解决。" + "course.assessment.question.rubricPlayground.applySuccess": { + "defaultMessage": "评分标准、提示和结果已成功应用。" }, - "course.assessment.question.scribing.ScribingQuestionForm.scribingQuestionWarning": { - "defaultMessage": "注意:PDF 文件的每一页都将创建为单个 Scribing 问题,每个问题都采用相同的问题详细信息。你可以选择将可选输入留空并在创建后返回再次编辑问题。" + "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { + "defaultMessage": "应用此评分标准将为所有学生答案分配成绩,包括此页面上尚未评估的答案。" }, - "course.assessment.question.scribing.ScribingQuestionForm.skillsFieldLabel": { - "defaultMessage": "技能" + "course.assessment.question.rubricPlayground.applyingRubricGradingData": { + "defaultMessage": "正在应用评分标准数据..." }, - "course.assessment.question.scribing.ScribingQuestionForm.staffOnlyCommentsFieldLabel": { - "defaultMessage": "仅限工作人员评论" + "course.assessment.question.rubricPlayground.categoryHeading": { + "defaultMessage": "类{index}" }, - "course.assessment.question.scribing.ScribingQuestionForm.submitButton": { - "defaultMessage": "提交" + "course.assessment.question.rubricPlayground.categoryName": { + "defaultMessage": "类别名称" }, - "course.assessment.question.scribing.ScribingQuestionForm.submitFailureMessage": { - "defaultMessage": "发生错误,请重试。" + "course.assessment.question.rubricPlayground.compare": { + "defaultMessage": "比较" }, - "course.assessment.question.scribing.ScribingQuestionForm.submittingMessage": { - "defaultMessage": "提交中..." + "course.assessment.question.rubricPlayground.comparingRevisions": { + "defaultMessage": "正在比较 {count} 个版本" }, - "course.assessment.question.scribing.ScribingQuestionForm.titleFieldLabel": { - "defaultMessage": "标题" + "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { + "defaultMessage": "确认应用AI评分" }, - "course.assessment.question.scribing.ScribingQuestionForm.valueMoreThan1000Error": { - "defaultMessage": "值必须小于 1000。" + "course.assessment.question.rubricPlayground.confirmProceed": { + "defaultMessage": "您确定要继续吗?" }, - "course.assessment.question.textResponses.addSolution": { - "defaultMessage": "添加一个新答案" + "course.assessment.question.rubricPlayground.dismiss": { + "defaultMessage": "关闭" }, - "course.assessment.question.textResponses.allowFileUpload": { - "defaultMessage": "允许在回答中上传文件" + "course.assessment.question.rubricPlayground.evaluate": { + "defaultMessage": "评估" }, - "course.assessment.question.textResponses.deleteSolution": { - "defaultMessage": "删除答案" + "course.assessment.question.rubricPlayground.evaluateAll": { + "defaultMessage": "全部评估 ({count})" }, - "course.assessment.question.textResponses.exactMatch": { - "defaultMessage": "精确匹配" + "course.assessment.question.rubricPlayground.evaluateRemaining": { + "defaultMessage": "评估剩余 ({count})" }, - "course.assessment.question.textResponses.fileUploadNote": { - "defaultMessage": "注意:无法自动评分文件上传题,将始终给出最高分。" + "course.assessment.question.rubricPlayground.evaluating": { + "defaultMessage": "评估中" }, - "course.assessment.question.textResponses.noSolutionsNote": { - "defaultMessage": "如果没有提供答案,自动打分器将始终给出最高分。" + "course.assessment.question.rubricPlayground.feedback": { + "defaultMessage": "反馈" }, - "course.assessment.question.textResponses.atLeastOneSolutionNote": { - "defaultMessage": "如果提供了至少一个答案,学生只能输入纯文本。" + "course.assessment.question.rubricPlayground.gradingCategories": { + "defaultMessage": "评分类别" }, - "course.assessment.question.textResponses.exactMatchSolutionNote": { - "defaultMessage": "如果选择\"精确匹配\",多行答案必须与学生答案完全匹配,才能被评为正确。如果找到匹配,自动打分器将给出指定分数,不再考虑其他答案。" + "course.assessment.question.rubricPlayground.gradingPrompt": { + "defaultMessage": "评分提示" }, - "course.assessment.question.textResponses.solutionGradesExceedMaximumGradeNote": { - "defaultMessage": "如果学生答案满足多个答案条件,自动打分器将给出这些答案分数的总和,最高不超过题目的最高分。" + "course.assessment.question.rubricPlayground.gradingPromptDescription": { + "defaultMessage": "指导AI进行评分和提供反馈的说明。" }, - "course.assessment.question.textResponses.grade": { - "defaultMessage": "得分" + "course.assessment.question.rubricPlayground.max": { + "defaultMessage": "最大值" }, - "course.assessment.question.textResponses.keyword": { - "defaultMessage": "关键词" + "course.assessment.question.rubricPlayground.modelAnswer": { + "defaultMessage": "标准答案" }, - "course.assessment.question.textResponses.newSolutionCannotUndo": { - "defaultMessage": "这是一个新答案。如果在保存前删除,它会立即消失。" + "course.assessment.question.rubricPlayground.modelAnswerDescription": { + "defaultMessage": "每个类别获得最高分的示例。" }, - "course.assessment.question.textResponses.solution": { - "defaultMessage": "答案" + "course.assessment.question.rubricPlayground.noAnswers": { + "defaultMessage": "尚未添加示例答案。添加一些以开始。" }, - "course.assessment.question.textResponses.solutionType": { - "defaultMessage": "答案类型" + "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { + "defaultMessage": "您选择应用的评分标准不是此页面上保存的最新版本。" }, - "course.assessment.question.textResponses.solutionWillBeDeleted": { - "defaultMessage": "保存更改后,该答案将被删除。" + "course.assessment.question.rubricPlayground.questionGrade": { + "defaultMessage": "成绩" }, - "course.assessment.question.textResponses.solutions": { - "defaultMessage": "答案" + "course.assessment.question.rubricPlayground.reevaluate": { + "defaultMessage": "重新评估" }, - "course.assessment.question.textResponses.solutionsHint": { - "defaultMessage": "添加答案以启用自动打分。" + "course.assessment.question.rubricPlayground.reevaluateAll": { + "defaultMessage": "全部重新评估 ({count})" }, - "course.assessment.question.textResponses.undoDeleteSolution": { - "defaultMessage": "撤销删除答案" + "course.assessment.question.rubricPlayground.rubricPlayground": { + "defaultMessage": "评分标准试验场" }, - "course.assessment.question.textResponses.zeroGrade": { - "defaultMessage": "0.0" + "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { + "defaultMessage": "示例答案评估" }, - "course.assessment.question.textResponses.templateText": { - "defaultMessage": "模版" + "course.assessment.question.rubricPlayground.savedRubric": { + "defaultMessage": "已保存的评分标准,{date}" }, - "course.assessment.question.textResponses.templateTextDescription": { - "defaultMessage": "学生第一次尝试此题时,答题区域中出现的文字。" + "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { + "defaultMessage": "按学生姓名或成绩搜索答案" }, - "course.assessment.question.textResponses.regex": { - "defaultMessage": "正则表达式" + "course.assessment.question.rubricPlayground.student": { + "defaultMessage": "学生" }, - "course.assessment.question.textResponses.invalidRegex": { - "defaultMessage": "无效的正则表达式" + "course.assessment.question.rubricPlayground.totalGrade": { + "defaultMessage": "总分" }, - "course.assessment.question.textResponses.spreadsheetFormula": { - "defaultMessage": "电子表格公式" + "course.assessment.question.rubricPlayground.viewEditRubric": { + "defaultMessage": "查看 / 编辑评分标准" }, - "course.assessment.question.textResponses.testSpreadsheet": { - "defaultMessage": "测试电子表格" + "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { + "defaultMessage": "在此处编写答案" }, - "course.assessment.question.textResponses.testSpreadsheetDescription": { - "defaultMessage": "需要上传测试电子表格以自动评分学生答案。" + "course.assessment.question.rubricPlayground.writeCustomAnswer": { + "defaultMessage": "编写自定义答案" }, - "course.assessment.question.textResponses.testSpreadsheetRequired": { - "defaultMessage": "请上传测试电子表格文件。" + "course.assessment.question.scribing.ScribingQuestionForm.cannotBeBlankValidationError": { + "defaultMessage": "不能留空。" }, - "course.assessment.question.textResponses.spreadsheetAdvancedOptions": { - "defaultMessage": "高级选项" + "course.assessment.question.scribing.ScribingQuestionForm.chooseFileButton": { + "defaultMessage": "选择文件" }, - "course.assessment.question.textResponses.spreadsheetRandomization": { - "defaultMessage": "启用随机化" + "course.assessment.question.scribing.ScribingQuestionForm.descriptionFieldLabel": { + "defaultMessage": "描述" }, - "course.assessment.question.textResponses.spreadsheetRandomizationDescription": { - "defaultMessage": "启用后,自动评分时将随机调整部分电子表格数值。这有助于确保公式确实计算出正确答案,而不是预设值。" + "course.assessment.question.scribing.ScribingQuestionForm.fetchFailureMessage": { + "defaultMessage": "发生错误,请重试。" }, - "course.assessment.question.textResponses.numberOfRandomTests": { - "defaultMessage": "随机测试次数" + "course.assessment.question.scribing.ScribingQuestionForm.fileAttachmentRequired": { + "defaultMessage": "需要文件附件。" + }, + "course.assessment.question.scribing.ScribingQuestionForm.fileUploaded": { + "defaultMessage": "已上传文件:" + }, + "course.assessment.question.scribing.ScribingQuestionForm.lessThanEqualZeroValidationError": { + "defaultMessage": "值必须大于 0。" + }, + "course.assessment.question.scribing.ScribingQuestionForm.maximumGradeFieldLabel": { + "defaultMessage": "最高等级" + }, + "course.assessment.question.scribing.ScribingQuestionForm.noFileChosenMessage": { + "defaultMessage": "没有选中任何文件" + }, + "course.assessment.question.scribing.ScribingQuestionForm.positiveNumberValidationError": { + "defaultMessage": "必须为正值。" + }, + "course.assessment.question.scribing.ScribingQuestionForm.resolveErrorsMessage": { + "defaultMessage": "此表单有错误,请在提交前解决。" + }, + "course.assessment.question.scribing.ScribingQuestionForm.scribingQuestionWarning": { + "defaultMessage": "注意:PDF 文件的每一页都将创建为单个 Scribing 问题,每个问题都采用相同的问题详细信息。你可以选择将可选输入留空并在创建后返回再次编辑问题。" + }, + "course.assessment.question.scribing.ScribingQuestionForm.skillsFieldLabel": { + "defaultMessage": "技能" + }, + "course.assessment.question.scribing.ScribingQuestionForm.staffOnlyCommentsFieldLabel": { + "defaultMessage": "仅限工作人员评论" + }, + "course.assessment.question.scribing.ScribingQuestionForm.submitButton": { + "defaultMessage": "提交" + }, + "course.assessment.question.scribing.ScribingQuestionForm.submitFailureMessage": { + "defaultMessage": "发生错误,请重试。" + }, + "course.assessment.question.scribing.ScribingQuestionForm.submittingMessage": { + "defaultMessage": "提交中..." + }, + "course.assessment.question.scribing.ScribingQuestionForm.titleFieldLabel": { + "defaultMessage": "标题" + }, + "course.assessment.question.scribing.ScribingQuestionForm.valueMoreThan1000Error": { + "defaultMessage": "值必须小于 1000。" }, "course.assessment.question.textResponses.SpreadsheetManager.fixedRandomSeed": { "defaultMessage": "固定随机种子" @@ -2324,24 +2441,54 @@ "course.assessment.question.textResponses.SpreadsheetManager.fixedTimestampDescription": { "defaultMessage": "勾选后,使用当前日期/时间的公式(NOW、TODAY 等)将改用此特定日期/时间。" }, - "course.assessment.question.textResponses.randomizationMode": { - "defaultMessage": "自动评分值配置" + "course.assessment.question.textResponses.addSolution": { + "defaultMessage": "添加一个新答案" }, - "course.assessment.question.textResponses.randomizationModeDescription": { - "defaultMessage": "点击任意单元格以配置在自动评分时如何替换或随机化其值。" + "course.assessment.question.textResponses.allowFileUpload": { + "defaultMessage": "允许在回答中上传文件" }, - "course.assessment.question.textResponses.randomizationModeClearAll": { - "defaultMessage": "全部清除" + "course.assessment.question.textResponses.atLeastOneSolutionNote": { + "defaultMessage": "如果提供了至少一个答案,学生只能输入纯文本。" }, - "course.assessment.question.textResponses.randomizationModeRestoreDefaults": { - "defaultMessage": "恢复默认值" + "course.assessment.question.textResponses.dateRandomizationMode": { + "defaultMessage": "使用随机日期值" }, - "course.assessment.question.textResponses.randomizationModePopoverTitle": { - "defaultMessage": "在自动评分时..." + "course.assessment.question.textResponses.deleteSolution": { + "defaultMessage": "删除答案" + }, + "course.assessment.question.textResponses.exactMatch": { + "defaultMessage": "精确匹配" + }, + "course.assessment.question.textResponses.exactMatchSolutionNote": { + "defaultMessage": "如果选择\"精确匹配\",多行答案必须与学生答案完全匹配,才能被评为正确。如果找到匹配,自动打分器将给出指定分数,不再考虑其他答案。" + }, + "course.assessment.question.textResponses.fileUploadNote": { + "defaultMessage": "注意:无法自动评分文件上传题,将始终给出最高分。" + }, + "course.assessment.question.textResponses.grade": { + "defaultMessage": "得分" + }, + "course.assessment.question.textResponses.invalidRegex": { + "defaultMessage": "无效的正则表达式" + }, + "course.assessment.question.textResponses.keyword": { + "defaultMessage": "关键词" + }, + "course.assessment.question.textResponses.newSolutionCannotUndo": { + "defaultMessage": "这是一个新答案。如果在保存前删除,它会立即消失。" }, "course.assessment.question.textResponses.noRandomizationMode": { "defaultMessage": "保留原始值" }, + "course.assessment.question.textResponses.noSolutionsNote": { + "defaultMessage": "如果没有提供答案,自动打分器将始终给出最高分。" + }, + "course.assessment.question.textResponses.numberOfRandomTests": { + "defaultMessage": "随机测试次数" + }, + "course.assessment.question.textResponses.numericRandomizationMode": { + "defaultMessage": "使用随机数值" + }, "course.assessment.question.textResponses.overrideRandomizationMode": { "defaultMessage": "覆盖为指定值" }, @@ -2351,170 +2498,98 @@ "course.assessment.question.textResponses.overrideValue": { "defaultMessage": "值" }, - "course.assessment.question.textResponses.numericRandomizationMode": { - "defaultMessage": "使用随机数值" + "course.assessment.question.textResponses.randomizationMode": { + "defaultMessage": "自动评分值配置" }, - "course.assessment.question.textResponses.roundToInteger": { - "defaultMessage": "取整到整数" + "course.assessment.question.textResponses.randomizationModeClearAll": { + "defaultMessage": "全部清除" }, - "course.assessment.question.textResponses.stringRandomizationMode": { - "defaultMessage": "随机化值中的字符" + "course.assessment.question.textResponses.randomizationModeDescription": { + "defaultMessage": "点击任意单元格以配置在自动评分时如何替换或随机化其值。" }, - "course.assessment.question.textResponses.stringRandomizationModeDescription": { - "defaultMessage": "要随机化的每个字符类可以用同一类中的不同字符替换。" + "course.assessment.question.textResponses.randomizationModePopoverTitle": { + "defaultMessage": "在自动评分时..." }, - "course.assessment.question.textResponses.randomizeDigits": { + "course.assessment.question.textResponses.randomizationModeRestoreDefaults": { + "defaultMessage": "恢复默认值" + }, + "course.assessment.question.textResponses.randomizeDigits": { "defaultMessage": "随机化数字 (0-9)" }, "course.assessment.question.textResponses.randomizeLetters": { "defaultMessage": "随机化字母 (a-z)" }, - "course.assessment.question.textResponses.dateRandomizationMode": { - "defaultMessage": "使用随机日期值" + "course.assessment.question.textResponses.regex": { + "defaultMessage": "正则表达式" }, "course.assessment.question.textResponses.roundToDay": { "defaultMessage": "向下取整到整天 (00:00 UTC)" }, + "course.assessment.question.textResponses.roundToInteger": { + "defaultMessage": "取整到整数" + }, "course.assessment.question.textResponses.shuffleRandomizationMode": { "defaultMessage": "与其他单元格随机排列值" }, "course.assessment.question.textResponses.shuffleRandomizationModeDescription": { "defaultMessage": "每个标记为\"随机排列\"的单元格值将被随机重新分配。总体分布保持不变,但每个值可能出现在不同的单元格中。" }, - "course.assessment.question.rubricPlayground.rubricPlayground": { - "defaultMessage": "评分标准试验场" - }, - "course.assessment.question.rubricPlayground.savedRubric": { - "defaultMessage": "已保存的评分标准,{date}" - }, - "course.assessment.question.rubricPlayground.viewEditRubric": { - "defaultMessage": "查看 / 编辑评分标准" - }, - "course.assessment.question.rubricPlayground.evaluate": { - "defaultMessage": "评估" - }, - "course.assessment.question.rubricPlayground.compare": { - "defaultMessage": "比较" - }, - "course.assessment.question.rubricPlayground.apply": { - "defaultMessage": "应用" - }, - "course.assessment.question.rubricPlayground.confirmAIGradingApplication": { - "defaultMessage": "确认应用AI评分" - }, - "course.assessment.question.rubricPlayground.applyingRubricGradingData": { - "defaultMessage": "正在应用评分标准数据..." - }, - "course.assessment.question.rubricPlayground.applySuccess": { - "defaultMessage": "评分标准、提示和结果已成功应用。" - }, - "course.assessment.question.rubricPlayground.applyFailure": { - "defaultMessage": "应用评分结果失败" - }, - "course.assessment.question.rubricPlayground.notLatestRevisionWarning": { - "defaultMessage": "您选择应用的评分标准不是此页面上保存的最新版本。" - }, - "course.assessment.question.rubricPlayground.applyWillGradeAllAnswers": { - "defaultMessage": "应用此评分标准将为所有学生答案分配成绩,包括此页面上尚未评估的答案。" - }, - "course.assessment.question.rubricPlayground.confirmProceed": { - "defaultMessage": "您确定要继续吗?" - }, - "course.assessment.question.rubricPlayground.sampleAnswerEvaluations": { - "defaultMessage": "示例答案评估" - }, - "course.assessment.question.rubricPlayground.addSampleAnswers": { - "defaultMessage": "添加示例答案" - }, - "course.assessment.question.rubricPlayground.evaluateAll": { - "defaultMessage": "全部评估 ({count})" - }, - "course.assessment.question.rubricPlayground.reevaluateAll": { - "defaultMessage": "全部重新评估 ({count})" - }, - "course.assessment.question.rubricPlayground.evaluateRemaining": { - "defaultMessage": "评估剩余 ({count})" - }, - "course.assessment.question.rubricPlayground.comparingRevisions": { - "defaultMessage": "正在比较 {count} 个版本" - }, - "course.assessment.question.rubricPlayground.addSampleAnswersTitle": { - "defaultMessage": "添加示例答案" - }, - "course.assessment.question.rubricPlayground.add": { - "defaultMessage": "添加" - }, - "course.assessment.question.rubricPlayground.addExistingAnswers": { - "defaultMessage": "添加现有答案" - }, - "course.assessment.question.rubricPlayground.student": { - "defaultMessage": "学生" - }, - "course.assessment.question.rubricPlayground.questionGrade": { - "defaultMessage": "成绩" - }, - "course.assessment.question.rubricPlayground.categoryHeading": { - "defaultMessage": "类{index}" - }, - "course.assessment.question.rubricPlayground.answer": { + "course.assessment.question.textResponses.solution": { "defaultMessage": "答案" }, - "course.assessment.question.rubricPlayground.searchAnswersPlaceholder": { - "defaultMessage": "按学生姓名或成绩搜索答案" - }, - "course.assessment.question.rubricPlayground.addRandomStudentAnswers": { - "defaultMessage": "添加 {inputComponent} 个随机学生答案" + "course.assessment.question.textResponses.solutionGradesExceedMaximumGradeNote": { + "defaultMessage": "如果学生答案满足多个答案条件,自动打分器将给出这些答案分数的总和,最高不超过题目的最高分。" }, - "course.assessment.question.rubricPlayground.writeCustomAnswer": { - "defaultMessage": "编写自定义答案" + "course.assessment.question.textResponses.solutionType": { + "defaultMessage": "答案类型" }, - "course.assessment.question.rubricPlayground.writeAnswerPlaceholder": { - "defaultMessage": "在此处编写答案" + "course.assessment.question.textResponses.solutionWillBeDeleted": { + "defaultMessage": "保存更改后,该答案将被删除。" }, - "course.assessment.question.rubricPlayground.dismiss": { - "defaultMessage": "关闭" + "course.assessment.question.textResponses.solutions": { + "defaultMessage": "答案" }, - "course.assessment.question.rubricPlayground.noAnswers": { - "defaultMessage": "尚未添加示例答案。添加一些以开始。" + "course.assessment.question.textResponses.solutionsHint": { + "defaultMessage": "添加答案以启用自动打分。" }, - "course.assessment.question.rubricPlayground.reevaluate": { - "defaultMessage": "重新评估" + "course.assessment.question.textResponses.spreadsheetAdvancedOptions": { + "defaultMessage": "高级选项" }, - "course.assessment.question.rubricPlayground.totalGrade": { - "defaultMessage": "总分" + "course.assessment.question.textResponses.spreadsheetFormula": { + "defaultMessage": "电子表格公式" }, - "course.assessment.question.rubricPlayground.feedback": { - "defaultMessage": "反馈" + "course.assessment.question.textResponses.spreadsheetRandomization": { + "defaultMessage": "启用随机化" }, - "course.assessment.question.rubricPlayground.evaluating": { - "defaultMessage": "评估中" + "course.assessment.question.textResponses.spreadsheetRandomizationDescription": { + "defaultMessage": "启用后,自动评分时将随机调整部分电子表格数值。这有助于确保公式确实计算出正确答案,而不是预设值。" }, - "course.assessment.question.rubricPlayground.gradingPrompt": { - "defaultMessage": "评分提示" + "course.assessment.question.textResponses.stringRandomizationMode": { + "defaultMessage": "随机化值中的字符" }, - "course.assessment.question.rubricPlayground.gradingPromptDescription": { - "defaultMessage": "指导AI进行评分和提供反馈的说明。" + "course.assessment.question.textResponses.stringRandomizationModeDescription": { + "defaultMessage": "要随机化的每个字符类可以用同一类中的不同字符替换。" }, - "course.assessment.question.rubricPlayground.modelAnswer": { - "defaultMessage": "标准答案" + "course.assessment.question.textResponses.templateText": { + "defaultMessage": "模版" }, - "course.assessment.question.rubricPlayground.modelAnswerDescription": { - "defaultMessage": "每个类别获得最高分的示例。" + "course.assessment.question.textResponses.templateTextDescription": { + "defaultMessage": "学生第一次尝试此题时,答题区域中出现的文字。" }, - "course.assessment.question.rubricPlayground.gradingCategories": { - "defaultMessage": "评分类别" + "course.assessment.question.textResponses.testSpreadsheet": { + "defaultMessage": "测试电子表格" }, - "course.assessment.question.rubricPlayground.addNewCategory": { - "defaultMessage": "添加新类别" + "course.assessment.question.textResponses.testSpreadsheetDescription": { + "defaultMessage": "需要上传测试电子表格以自动评分学生答案。" }, - "course.assessment.question.rubricPlayground.categoryName": { - "defaultMessage": "类别名称" + "course.assessment.question.textResponses.testSpreadsheetRequired": { + "defaultMessage": "请上传测试电子表格文件。" }, - "course.assessment.question.rubricPlayground.max": { - "defaultMessage": "最大值" + "course.assessment.question.textResponses.undoDeleteSolution": { + "defaultMessage": "撤销删除答案" }, - "course.assessment.question.rubricPlayground.addNewGrade": { - "defaultMessage": "添加新分数" + "course.assessment.question.textResponses.zeroGrade": { + "defaultMessage": "0.0" }, "course.assessment.session.assessmentNotStarted": { "defaultMessage": "测试尚未开始,请在 {startDate} 之后再来。" @@ -2651,14 +2726,14 @@ "course.assessment.show.generate": { "defaultMessage": "生成问题" }, - "course.assessment.show.generateTooltip": { - "defaultMessage": "与 Codaveri AI 合作创建问题" + "course.assessment.show.generateFromProgrammingQuestion": { + "defaultMessage": "使用 Codaveri AI 生成类似问题" }, "course.assessment.show.generateFromQuestion": { "defaultMessage": "使用 AI 生成类似问题" }, - "course.assessment.show.generateFromProgrammingQuestion": { - "defaultMessage": "使用 Codaveri AI 生成类似问题" + "course.assessment.show.generateTooltip": { + "defaultMessage": "与 Codaveri AI 合作创建问题" }, "course.assessment.show.gradedTestCases": { "defaultMessage": "为测试用例打分" @@ -2673,7 +2748,7 @@ "defaultMessage": "下面的问题。" }, "course.assessment.show.headsUpExistingSubmissions": { - "defaultMessage": "注意——提交已存在!" + "defaultMessage": "注意--提交已存在!" }, "course.assessment.show.hideOptions": { "defaultMessage": "隐藏选项" @@ -2792,15 +2867,15 @@ "course.assessment.show.showMcqMrqSolution": { "defaultMessage": "显示多选题解决方案" }, - "course.assessment.show.showRubricToStudents": { - "defaultMessage": "向学生显示评分细则" - }, "course.assessment.show.showMcqSubmitResult": { "defaultMessage": "显示多选题提交结果" }, "course.assessment.show.showOptions": { "defaultMessage": "显示选项" }, + "course.assessment.show.showRubricToStudents": { + "defaultMessage": "向学生显示评分细则" + }, "course.assessment.show.sureChangingQuestionType": { "defaultMessage": "确定要更改此问题类型吗?" }, @@ -2927,107 +3002,65 @@ "course.assessment.skills.SkillsTable.uncategorised": { "defaultMessage": "未分类技能" }, - "course.assessment.liveFeedback.questionTitle": { - "defaultMessage": "题目 {index}" - }, - "course.assessment.liveFeedback.messageTimingTitle": { - "defaultMessage": "生成时间:{usedAt}" + "course.assessment.statistics.ancestorFail": { + "defaultMessage": "无法获取此测试的过去结果。" }, - "course.assessment.liveFeedback.liveFeedbackName": { - "defaultMessage": "实时反馈" + "course.assessment.statistics.ancestorSelect.current": { + "defaultMessage": "当前" }, - "course.assessment.liveFeedback.comments": { - "defaultMessage": "评论" + "course.assessment.statistics.ancestorSelect.fromCourse": { + "defaultMessage": "来自 {courseTitle}" }, - "course.assessment.liveFeedback.lineHeader": { - "defaultMessage": "第 {lineNumber} 行" + "course.assessment.statistics.ancestorSelect.subtitle": { + "defaultMessage": "与此测验的过去版本进行比较:" }, - "course.assessment.submission.GetHelpChatPage.chatInputText": { - "defaultMessage": "我们如何帮助您?" + "course.assessment.statistics.ancestorSelect.title": { + "defaultMessage": "复制历史" }, - "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { - "defaultMessage": "剩余 {numMessages} / {maxMessages} 条消息" + "course.assessment.statistics.ancestorStatisticsFail": { + "defaultMessage": "获取祖先统计数据失败。" }, - "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { - "defaultMessage": "您已达到此问题的消息限制。" + "course.assessment.statistics.answers": { + "defaultMessage": "答案" }, - "course.assessment.submission.GetHelpChatPage.codeUpdated": { - "defaultMessage": "代码已更新" + "course.assessment.statistics.attemptCount": { + "defaultMessage": "尝试次数" }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { - "defaultMessage": "第 {lineNumber} 行" + "course.assessment.statistics.attempts.filename": { + "defaultMessage": "{assessment} 的题目尝试统计" }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { - "defaultMessage": "{filename}:{lineNumber}" + "course.assessment.statistics.attempts.greenCellLegend": { + "defaultMessage": "正确" }, - "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { - "defaultMessage": "上面的聊天已结束。开始新的聊天?" + "course.assessment.statistics.attempts.redCellLegend": { + "defaultMessage": "错误" }, - "course.assessment.plagiarism.plagiarism": { - "defaultMessage": "抄袭结果" + "course.assessment.statistics.closePrompt": { + "defaultMessage": "关闭" }, - "course.assessment.plagiarism.status": { - "defaultMessage": "抄袭检查状态" + "course.assessment.statistics.duplicationHistory": { + "defaultMessage": "复制历史" }, - "course.assessment.plagiarism.lastRunTime": { - "defaultMessage": "上次运行时间:{date}" + "course.assessment.statistics.fail": { + "defaultMessage": "获取统计数据失败。" }, - "course.assessment.plagiarism.start": { - "defaultMessage": "新的抄袭检查" + "course.assessment.statistics.gradeDistribution": { + "defaultMessage": "成绩分布" }, - "course.assessment.plagiarism.notStarted": { - "defaultMessage": "尚未运行抄袭检查" + "course.assessment.statistics.gradeViolin.datasetLabel": { + "defaultMessage": "分布" }, - "course.assessment.plagiarism.confirmStartTitle": { - "defaultMessage": "确认抄袭检查?" + "course.assessment.statistics.gradeViolin.xAxisLabel": { + "defaultMessage": "得分" }, - "course.assessment.plagiarism.confirmStartMessage": { - "defaultMessage": "运行新的抄袭检查将删除之前的结果。" + "course.assessment.statistics.gradeViolin.yAxisLabel": { + "defaultMessage": "提交" }, - "course.assessment.plagiarism.results": { - "defaultMessage": "抄袭结果(提交之间的相似度)" + "course.assessment.statistics.grader": { + "defaultMessage": "评分者" }, - "course.assessment.plagiarism.baseSubmission": { - "defaultMessage": "基准提交" - }, - "course.assessment.plagiarism.comparedSubmission": { - "defaultMessage": "比较提交" - }, - "course.assessment.plagiarism.similarityScore": { - "defaultMessage": "相似性分数" - }, - "course.assessment.plagiarism.actions": { - "defaultMessage": "操作" - }, - "course.assessment.plagiarism.viewReport": { - "defaultMessage": "查看报告" - }, - "course.assessment.plagiarism.downloadPdf": { - "defaultMessage": "下载 PDF" - }, - "course.assessment.plagiarism.searchByStudentName": { - "defaultMessage": "按学生姓名搜索" - }, - "course.assessment.plagiarism.showSelfPlagiarism": { - "defaultMessage": "包含自我抄袭比较(同一学生,不同课程)" - }, - "course.assessment.statistics.answers": { - "defaultMessage": "答案" - }, - "course.assessment.statistics.attempts.filename": { - "defaultMessage": "{assessment} 的题目尝试统计" - }, - "course.assessment.statistics.attempts.greenCellLegend": { - "defaultMessage": "正确" - }, - "course.assessment.statistics.attempts.redCellLegend": { - "defaultMessage": "错误" - }, - "course.assessment.statistics.closePrompt": { - "defaultMessage": "关闭" - }, - "course.assessment.statistics.grader": { - "defaultMessage": "评分者" + "course.assessment.statistics.gradesPerQuestion": { + "defaultMessage": "每题分数" }, "course.assessment.statistics.grayCellLegend": { "defaultMessage": "未决定 (问题无法自动评分)" @@ -3035,12 +3068,21 @@ "course.assessment.statistics.group": { "defaultMessage": "组别" }, + "course.assessment.statistics.header": { + "defaultMessage": "{title} 的统计数据" + }, + "course.assessment.statistics.includePhantom": { + "defaultMessage": "包括虚拟学生" + }, "course.assessment.statistics.legendHigherusage": { "defaultMessage": "使用较多" }, "course.assessment.statistics.legendLowerUsage": { "defaultMessage": "使用较少" }, + "course.assessment.statistics.liveFeedback": { + "defaultMessage": "获取帮助" + }, "course.assessment.statistics.liveFeedback.filename": { "defaultMessage": "{assessment} 的题目实时反馈统计" }, @@ -3077,66 +3119,6 @@ "course.assessment.statistics.questionIndex": { "defaultMessage": "题目 {index}" }, - "course.assessment.statistics.totalFeedbackCount": { - "defaultMessage": "总计" - }, - "course.assessment.statistics.totalGrade": { - "defaultMessage": "总分" - }, - "course.assessment.statistics.workflowState": { - "defaultMessage": "状态" - }, - "course.assessment.statistics.ancestorFail": { - "defaultMessage": "无法获取此测试的过去结果。" - }, - "course.assessment.statistics.ancestorStatisticsFail": { - "defaultMessage": "获取祖先统计数据失败。" - }, - "course.assessment.statistics.fail": { - "defaultMessage": "获取统计数据失败。" - }, - "course.assessment.statistics.gradeDistribution": { - "defaultMessage": "成绩分布" - }, - "course.assessment.statistics.gradeViolin.datasetLabel": { - "defaultMessage": "分布" - }, - "course.assessment.statistics.gradeViolin.xAxisLabel": { - "defaultMessage": "得分" - }, - "course.assessment.statistics.gradeViolin.yAxisLabel": { - "defaultMessage": "提交" - }, - "course.assessment.statistics.ancestorSelect.current": { - "defaultMessage": "当前" - }, - "course.assessment.statistics.ancestorSelect.fromCourse": { - "defaultMessage": "来自 {courseTitle}" - }, - "course.assessment.statistics.ancestorSelect.subtitle": { - "defaultMessage": "与此测验的过去版本进行比较:" - }, - "course.assessment.statistics.ancestorSelect.title": { - "defaultMessage": "复制历史" - }, - "course.assessment.statistics.attemptCount": { - "defaultMessage": "尝试次数" - }, - "course.assessment.statistics.duplicationHistory": { - "defaultMessage": "复制历史" - }, - "course.assessment.statistics.gradesPerQuestion": { - "defaultMessage": "每题分数" - }, - "course.assessment.statistics.includePhantom": { - "defaultMessage": "包括虚拟学生" - }, - "course.assessment.statistics.liveFeedback": { - "defaultMessage": "获取帮助" - }, - "course.assessment.statistics.header": { - "defaultMessage": "{title} 的统计数据" - }, "course.assessment.statistics.statistics": { "defaultMessage": "统计数据" }, @@ -3158,9 +3140,24 @@ "course.assessment.statistics.submissionTimeGradeChart.xAxisLabel.withoutDeadline": { "defaultMessage": "提交日期" }, + "course.assessment.statistics.totalFeedbackCount": { + "defaultMessage": "总计" + }, + "course.assessment.statistics.totalGrade": { + "defaultMessage": "总分" + }, + "course.assessment.statistics.workflowState": { + "defaultMessage": "状态" + }, "course.assessment.submission.Annotations.comment": { "defaultMessage": "添加评论" }, + "course.assessment.submission.Answer.missingAnswer": { + "defaultMessage": "此问题没有已提交的答案--可能是因为提交之后又添加了新问题。" + }, + "course.assessment.submission.Answer.rendererNotImplemented": { + "defaultMessage": "此问题类型的显示尚未实现。" + }, "course.assessment.submission.CodaveriFeedbackStatus.codaveriFeedbackStatus": { "defaultMessage": "Codaveri反馈状态" }, @@ -3185,38 +3182,35 @@ "course.assessment.submission.FileInput.uploadLabel": { "defaultMessage": "拖放或点击上传文件" }, - "course.assessment.submission.ImportedFileView.deleteConfirmation": { - "defaultMessage": "你确定要删除此文件吗?" - }, - "course.assessment.submission.ImportedFileView.noFiles": { - "defaultMessage": "未上传文件。" + "course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber": { + "defaultMessage": "{filename}:{lineNumber}" }, - "course.assessment.submission.ImportedFileView.uploadedFiles": { - "defaultMessage": "上传的文件:" + "course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber": { + "defaultMessage": "第 {lineNumber} 行" }, - "course.assessment.submission.Answer.missingAnswer": { - "defaultMessage": "此问题没有已提交的答案——可能是因为提交之后又添加了新问题。" + "course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired": { + "defaultMessage": "上面的聊天已结束。开始新的聊天?" }, - "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { - "defaultMessage": "没有过去的答案。" + "course.assessment.submission.GetHelpChatPage.chatInputText": { + "defaultMessage": "我们如何帮助您?" }, - "course.assessment.submission.Answer.rendererNotImplemented": { - "defaultMessage": "此问题类型的显示尚未实现。" + "course.assessment.submission.GetHelpChatPage.chatMessagesRemaining": { + "defaultMessage": "剩余 {numMessages} / {maxMessages} 条消息" }, - "course.assessment.submission.answerTooLarge": { - "defaultMessage": "答案过大" + "course.assessment.submission.GetHelpChatPage.codeUpdated": { + "defaultMessage": "代码已更新" }, - "course.assessment.submission.answerTooLargeError": { - "defaultMessage": "您的答案必须小于 2 MB。" + "course.assessment.submission.GetHelpChatPage.noChatMessagesRemaining": { + "defaultMessage": "您已达到此问题的消息限制。" }, - "course.assessment.submission.publishAutoFeedbackConfirmationHeader": { - "defaultMessage": "您即将发布 {count} 条自动编程反馈。" + "course.assessment.submission.ImportedFileView.deleteConfirmation": { + "defaultMessage": "你确定要删除此文件吗?" }, - "course.assessment.submission.publishAutoFeedbackConfirmationPleaseRate": { - "defaultMessage": "请评价本次测验自动编程反馈的整体质量。您的评分将帮助我们改进自动编程反馈生成,以便所有用户受益。" + "course.assessment.submission.ImportedFileView.noFiles": { + "defaultMessage": "未上传文件。" }, - "course.assessment.submission.publishAutoFeedbackSuccess": { - "defaultMessage": "所有自动编程反馈均已发布。" + "course.assessment.submission.ImportedFileView.uploadedFiles": { + "defaultMessage": "上传的文件:" }, "course.assessment.submission.SubmissionAnswer.viewPastAnswers": { "defaultMessage": "查看过去的答案" @@ -3266,9 +3260,6 @@ "course.assessment.submission.SubmissionsIndex.includePhantoms": { "defaultMessage": "包括旁听学生" }, - "lib.translations.myStudents": { - "defaultMessage": "我的学生" - }, "course.assessment.submission.SubmissionsIndex.phantom": { "defaultMessage": "旁听用户" }, @@ -3284,12 +3275,6 @@ "course.assessment.submission.SubmissionsIndex.remind": { "defaultMessage": "发送提醒邮件" }, - "lib.translations.staff": { - "defaultMessage": "职员" - }, - "lib.translations.students": { - "defaultMessage": "学生" - }, "course.assessment.submission.SubmissionsIndex.submissionStatus": { "defaultMessage": "提交状态" }, @@ -3479,6 +3464,15 @@ "course.assessment.submission.answerSubmitted": { "defaultMessage": "答案已提交" }, + "course.assessment.submission.answerTooLarge": { + "defaultMessage": "答案过大" + }, + "course.assessment.submission.answerTooLargeError": { + "defaultMessage": "您的答案必须小于 2 MB。" + }, + "course.assessment.submission.answers.AnswerHeader.noPastAnswers": { + "defaultMessage": "没有过去的答案。" + }, "course.assessment.submission.answers.ForumPostResponse.ForumCard.forumCardTitleTypeNoneSelected": { "defaultMessage": "论坛" }, @@ -3557,6 +3551,18 @@ "course.assessment.submission.answers.Programming.ProgrammingFile.sizeTooBig": { "defaultMessage": "文件太大,无法显示。" }, + "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete": { + "defaultMessage": "忽略" + }, + "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike": { + "defaultMessage": "不喜欢" + }, + "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike": { + "defaultMessage": "喜欢" + }, + "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading": { + "defaultMessage": "线 {linenum}" + }, "course.assessment.submission.attemptedAt": { "defaultMessage": "试图在" }, @@ -3578,14 +3584,14 @@ "course.assessment.submission.bonusEndAt": { "defaultMessage": "奖金结束于" }, - "course.assessment.submission.codaveriAutogradeFailure": { - "defaultMessage": "(T_T) 抱歉,codaveri 自动打分气罢工了。尝试在几分钟后再次提交你的代码或检查网络响应中的错误消息。" + "course.assessment.submission.checkAnswer": { + "defaultMessage": "检查答案" }, - "course.assessment.submission.liveFeedbackNoneGenerated": { - "defaultMessage": "问题 {questionIndex}:未生成反馈。" + "course.assessment.submission.checkAnswerWithLimit": { + "defaultMessage": "检查答案({attemptsLeft, plural, one {# 次} other {# 次}}剩余)" }, - "course.assessment.submission.liveFeedbackSuccess": { - "defaultMessage": "问题 {questionIndex}:反馈生成成功。" + "course.assessment.submission.codaveriAutogradeFailure": { + "defaultMessage": "(T_T) 抱歉,codaveri 自动打分气罢工了。尝试在几分钟后再次提交你的代码或检查网络响应中的错误消息。" }, "course.assessment.submission.comment.CodaveriCommentCard.finalise": { "defaultMessage": "完成并发布反馈" @@ -3611,14 +3617,14 @@ "course.assessment.submission.comment.CommentCard.deleteConfirmation": { "defaultMessage": "你确定要删除此评论吗?" }, - "course.assessment.submission.comment.CommentCard.save": { - "defaultMessage": "保存" + "course.assessment.submission.comment.CommentCard.isAiGenerated": { + "defaultMessage": "AI 生成的评论" }, "course.assessment.submission.comment.CommentCard.publish": { "defaultMessage": "发布" }, - "course.assessment.submission.comment.CommentCard.isAiGenerated": { - "defaultMessage": "AI 生成的评论" + "course.assessment.submission.comment.CommentCard.save": { + "defaultMessage": "保存" }, "course.assessment.submission.comment.CommentField.comment": { "defaultMessage": "评论" @@ -3635,18 +3641,6 @@ "course.assessment.submission.comments": { "defaultMessage": "注释" }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete": { - "defaultMessage": "忽略" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike": { - "defaultMessage": "不喜欢" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike": { - "defaultMessage": "喜欢" - }, - "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading": { - "defaultMessage": "线 {linenum}" - }, "course.assessment.submission.continue": { "defaultMessage": "继续" }, @@ -3773,6 +3767,12 @@ "course.assessment.submission.lateSubmission": { "defaultMessage": "这次提交是迟到的!你可能想对迟交的学生进行处罚。" }, + "course.assessment.submission.liveFeedbackNoneGenerated": { + "defaultMessage": "问题 {questionIndex}:未生成反馈。" + }, + "course.assessment.submission.liveFeedbackSuccess": { + "defaultMessage": "问题 {questionIndex}:反馈生成成功。" + }, "course.assessment.submission.loadingComment": { "defaultMessage": "正在加载评论字段..." }, @@ -3839,6 +3839,15 @@ "course.assessment.submission.publish": { "defaultMessage": "发布等级" }, + "course.assessment.submission.publishAutoFeedbackConfirmationHeader": { + "defaultMessage": "您即将发布 {count} 条自动编程反馈。" + }, + "course.assessment.submission.publishAutoFeedbackConfirmationPleaseRate": { + "defaultMessage": "请评价本次测验自动编程反馈的整体质量。您的评分将帮助我们改进自动编程反馈生成,以便所有用户受益。" + }, + "course.assessment.submission.publishAutoFeedbackSuccess": { + "defaultMessage": "所有自动编程反馈均已发布。" + }, "course.assessment.submission.publishConfirmation": { "defaultMessage": "你确定要发布所有 {graded} 评分提交({selectedUsers})吗?此操作不可逆转!所有评分的提交都将发布,用户将能够看到自己的成绩。" }, @@ -3854,14 +3863,14 @@ "course.assessment.submission.question": { "defaultMessage": "题" }, - "course.assessment.submission.questionNumber": { - "defaultMessage": "Q{数字}" + "course.assessment.submission.questionAnswer": { + "defaultMessage": "作答" }, "course.assessment.submission.questionDescription": { "defaultMessage": "描述" }, - "course.assessment.submission.questionAnswer": { - "defaultMessage": "作答" + "course.assessment.submission.questionNumber": { + "defaultMessage": "Q{数字}" }, "course.assessment.submission.readOnlyEditor.expandComments": { "defaultMessage": "展开所有评论" @@ -3884,15 +3893,6 @@ "course.assessment.submission.resetConfirmation": { "defaultMessage": "你确定要重置你的答案吗?这个操作是不可逆的,你将失去你当前为这个问题所做的所有工作。" }, - "course.assessment.submission.checkAnswer": { - "defaultMessage": "检查答案" - }, - "course.assessment.submission.checkAnswerWithLimit": { - "defaultMessage": "检查答案({attemptsLeft, plural, one {# 次} other {# 次}}剩余)" - }, - "course.assessment.submission.submitWithLimit": { - "defaultMessage": "提交({attemptsLeft, plural, one {# 次} other {# 次}}剩余)" - }, "course.assessment.submission.saveDraft": { "defaultMessage": "保存草稿" }, @@ -3947,21 +3947,24 @@ "course.assessment.submission.submitShortcut": { "defaultMessage": "(Ctrl+Enter) 或 (⌘+Enter)" }, + "course.assessment.submission.submitWithLimit": { + "defaultMessage": "提交({attemptsLeft, plural, one {# 次} other {# 次}}剩余)" + }, "course.assessment.submission.submitted": { "defaultMessage": "已提交" }, "course.assessment.submission.submittedAt": { "defaultMessage": "提交于" }, - "course.assessment.submission.unknown": { - "defaultMessage": "未知状态,请联系管理员" - }, "course.assessment.submission.totalGrade": { "defaultMessage": "总成绩" }, "course.assessment.submission.type": { "defaultMessage": "类型" }, + "course.assessment.submission.unknown": { + "defaultMessage": "未知状态,请联系管理员" + }, "course.assessment.submission.unmark": { "defaultMessage": "恢复提交" }, @@ -4157,9 +4160,6 @@ "course.asssessment.submission.submitNoQuestionExplain": { "defaultMessage": "标记为完成?" }, - "course.admin.NotificationSettings.component": { - "defaultMessage": "组件" - }, "course.componentTitles.course_achievements_component": { "defaultMessage": "成就" }, @@ -4244,15 +4244,6 @@ "course.courses.CourseAnnouncements.announcementHeader": { "defaultMessage": "公告" }, - "course.courses.CourseSuspendedAlert.header": { - "defaultMessage": "此课程已暂停。讲师仍可访问,但学生无法访问。" - }, - "course.courses.CourseSuspendedAlert.canSuspendMessage": { - "defaultMessage": "您可以在{link}页面解除暂停。" - }, - "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { - "defaultMessage": "如果您认为这是错误,请联系课程管理员或所有者请其解除暂停。" - }, "course.courses.CourseDisplay.noCourse": { "defaultMessage": "还没有课程..." }, @@ -4298,6 +4289,15 @@ "course.courses.CourseShow.instructorsHeader": { "defaultMessage": "导师" }, + "course.courses.CourseSuspendedAlert.canSuspendMessage": { + "defaultMessage": "您可以在{link}页面解除暂停。" + }, + "course.courses.CourseSuspendedAlert.cannotSuspendMessage": { + "defaultMessage": "如果您认为这是错误,请联系课程管理员或所有者请其解除暂停。" + }, + "course.courses.CourseSuspendedAlert.header": { + "defaultMessage": "此课程已暂停。讲师仍可访问,但学生无法访问。" + }, "course.courses.CourseUserItem.differentCourseNameHint": { "defaultMessage": "你看到的名字与你的账户名不同,因为该课程管理员是用此名称邀请你的。" }, @@ -4466,15 +4466,15 @@ "course.courses.SidebarItem.home": { "defaultMessage": "主页" }, + "course.courses.SidebarItem.scholaistic.assessments": { + "defaultMessage": "角色扮演评估" + }, "course.courses.SidebarItem.stories.learn": { "defaultMessage": "学习" }, "course.courses.SidebarItem.stories.missionControl": { "defaultMessage": "任务控制" }, - "course.courses.SidebarItem.scholaistic.assessments": { - "defaultMessage": "角色扮演评估" - }, "course.courses.TodoIgnoreButton.ignore.ignoreButtonText": { "defaultMessage": "忽视" }, @@ -4517,6 +4517,12 @@ "course.discussion.topics.CommentCard.deleteSuccess": { "defaultMessage": "已成功删除评论。" }, + "course.discussion.topics.CommentCard.isAiGenerated": { + "defaultMessage": "AI 生成的草稿评论" + }, + "course.discussion.topics.CommentCard.publish": { + "defaultMessage": "发布" + }, "course.discussion.topics.CommentCard.publishFailure": { "defaultMessage": "无法发布反馈。" }, @@ -4538,12 +4544,6 @@ "course.discussion.topics.CommentCard.updateSuccess": { "defaultMessage": "已成功更新评论。" }, - "course.discussion.topics.CommentCard.publish": { - "defaultMessage": "发布" - }, - "course.discussion.topics.CommentCard.isAiGenerated": { - "defaultMessage": "AI 生成的草稿评论" - }, "course.discussion.topics.CommentField.comment": { "defaultMessage": "评论" }, @@ -4715,15 +4715,15 @@ "course.duplication.Duplication.duplicateData": { "defaultMessage": "复制数据" }, - "course.duplication.Duplication.fromCourse": { - "defaultMessage": "复制数据从{courseTitle}" - }, "course.duplication.Duplication.duplicationDisabled": { "defaultMessage": "本课程禁止复制。" }, "course.duplication.Duplication.existingCourse": { "defaultMessage": "现有课程" }, + "course.duplication.Duplication.fromCourse": { + "defaultMessage": "复制数据从{courseTitle}" + }, "course.duplication.Duplication.items": { "defaultMessage": "已选项目" }, @@ -4817,21 +4817,6 @@ "course.enrolRequests.UserRequests.rejected": { "defaultMessage": "拒绝的注册请求" }, - "course.experiencePoints.downloadCsvButton": { - "defaultMessage": "下载 CSV" - }, - "course.experiencePoints.downloadFailure": { - "defaultMessage": "处理您的下载请求时发生错误。" - }, - "course.experiencePoints.downloadPending": { - "defaultMessage": "请稍候,正在处理您的下载请求。" - }, - "course.experiencePoints.downloadRequestSuccess": { - "defaultMessage": "您的下载请求已成功" - }, - "course.experiencePoints.filterByNameButton": { - "defaultMessage": "按姓名筛选" - }, "course.experiencePoints.disbursement.DisbursementForm.createDisbursementFailure": { "defaultMessage": "奖励经验值失败。" }, @@ -4952,6 +4937,21 @@ "course.experiencePoints.disbursement.ForumPostTable.voteTally": { "defaultMessage": "点赞总数" }, + "course.experiencePoints.downloadCsvButton": { + "defaultMessage": "下载 CSV" + }, + "course.experiencePoints.downloadFailure": { + "defaultMessage": "处理您的下载请求时发生错误。" + }, + "course.experiencePoints.downloadPending": { + "defaultMessage": "请稍候,正在处理您的下载请求。" + }, + "course.experiencePoints.downloadRequestSuccess": { + "defaultMessage": "您的下载请求已成功" + }, + "course.experiencePoints.filterByNameButton": { + "defaultMessage": "按姓名筛选" + }, "course.forum.FormShow.fetchTopicsFailure": { "defaultMessage": "无法检索论坛主题数据。" }, @@ -5012,392 +5012,740 @@ "course.forum.ForumTable.isSubscribed": { "defaultMessage": "已订阅?" }, - "course.forum.ForumTable.noForum": { - "defaultMessage": "暂无分论坛" + "course.forum.ForumTable.noForum": { + "defaultMessage": "暂无分论坛" + }, + "course.forum.ForumTable.posts": { + "defaultMessage": "帖子" + }, + "course.forum.ForumTable.topics": { + "defaultMessage": "主题" + }, + "course.forum.ForumTable.views": { + "defaultMessage": "阅读量" + }, + "course.forum.ForumTable.votes": { + "defaultMessage": "点赞数" + }, + "course.forum.ForumTopicEdit.editForum": { + "defaultMessage": "编辑主题" + }, + "course.forum.ForumTopicEdit.updateFailure": { + "defaultMessage": "未能更新主题。" + }, + "course.forum.ForumTopicEdit.updateSuccess": { + "defaultMessage": "主题 {title} 已更新。" + }, + "course.forum.ForumTopicForm.postAnonymously": { + "defaultMessage": "匿名发帖" + }, + "course.forum.ForumTopicForm.text": { + "defaultMessage": "文本" + }, + "course.forum.ForumTopicForm.title": { + "defaultMessage": "标题" + }, + "course.forum.ForumTopicForm.topicType": { + "defaultMessage": "主题类型" + }, + "course.forum.ForumTopicForm.topicType.announcement": { + "defaultMessage": "公告" + }, + "course.forum.ForumTopicForm.topicType.normal": { + "defaultMessage": "普通" + }, + "course.forum.ForumTopicForm.topicType.question": { + "defaultMessage": "问题" + }, + "course.forum.ForumTopicForm.topicType.sticky": { + "defaultMessage": "置顶" + }, + "course.forum.ForumTopicManagementButtons.deletionConfirm": { + "defaultMessage": "你确定要删除此主题\"{title}\"吗?" + }, + "course.forum.ForumTopicManagementButtons.deletionFailure": { + "defaultMessage": "无法删除主题 - {error}" + }, + "course.forum.ForumTopicManagementButtons.deletionSuccess": { + "defaultMessage": "主题 {title} 已删除。" + }, + "course.forum.ForumTopicNew.creationFailure": { + "defaultMessage": "创建主题失败。" + }, + "course.forum.ForumTopicNew.creationSuccess": { + "defaultMessage": "主题 {title} 已创建。" + }, + "course.forum.ForumTopicNew.newTopic": { + "defaultMessage": "新主题" + }, + "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptAction": { + "defaultMessage": "放弃修改" + }, + "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptMessage": { + "defaultMessage": "此文章已被编辑,你要放弃未保存的更改吗?" + }, + "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptTitle": { + "defaultMessage": "放弃未保存的更改吗?" + }, + "course.forum.ForumTopicPostEditActionButtons.emptyPost": { + "defaultMessage": "内容不能为空" + }, + "course.forum.ForumTopicPostEditActionButtons.updateFailure": { + "defaultMessage": "无法更新帖子 - {error}" + }, + "course.forum.ForumTopicPostEditActionButtons.updateSuccess": { + "defaultMessage": "帖子已被更新" + }, + "course.forum.ForumTopicPostForm.postAnonymously": { + "defaultMessage": "匿名发帖" + }, + "course.forum.ForumTopicPostManagementButtons.deletionConfirm": { + "defaultMessage": "你确定要删除此主题帖吗?" + }, + "course.forum.ForumTopicPostManagementButtons.deletionFailure": { + "defaultMessage": "无法删除主题 - {error}" + }, + "course.forum.ForumTopicPostManagementButtons.deletionSuccess": { + "defaultMessage": "该帖子已被删除。" + }, + "course.forum.ForumTopicPostNew.creationFailure": { + "defaultMessage": "无法创建帖子 - {error}" + }, + "course.forum.ForumTopicPostNew.creationSuccess": { + "defaultMessage": "帖子已创建。" + }, + "course.forum.ForumTopicPostNew.newPost": { + "defaultMessage": "创建新帖子" + }, + "course.forum.ForumTopicShow.fetchPostsFailure": { + "defaultMessage": "无法检索论坛主题数据。" + }, + "course.forum.ForumTopicShow.header": { + "defaultMessage": "论坛主题帖" + }, + "course.forum.ForumTopicShow.lockedNote": { + "defaultMessage": "你无法添加新帖子,因为该主题已被教学人员锁定。" + }, + "course.forum.ForumTopicShow.noPosts": { + "defaultMessage": "没有帖子" + }, + "course.forum.ForumTopicShow.topicResolved": { + "defaultMessage": "这个问题已经解决。" + }, + "course.forum.ForumTopicShow.topicUnresolved": { + "defaultMessage": "此问题未解决。" + }, + "course.forum.ForumTopicShow.topicUnresolvedNote": { + "defaultMessage": "将有帮助的帖子标记为答案以解决此问题。" + }, + "course.forum.ForumTopicTable.hidden": { + "defaultMessage": "该主题对学生是隐藏的。" + }, + "course.forum.ForumTopicTable.isSubscribed": { + "defaultMessage": "已订阅?" + }, + "course.forum.ForumTopicTable.lastPostedBy": { + "defaultMessage": "最后发表者" + }, + "course.forum.ForumTopicTable.locked": { + "defaultMessage": "该主题已关闭;它不再接受新的回复。" + }, + "course.forum.ForumTopicTable.noTopic": { + "defaultMessage": "没有主题" + }, + "course.forum.ForumTopicTable.posts": { + "defaultMessage": "帖子" + }, + "course.forum.ForumTopicTable.resolved": { + "defaultMessage": "问题(已解决)" + }, + "course.forum.ForumTopicTable.startedBy": { + "defaultMessage": "开始于" + }, + "course.forum.ForumTopicTable.topics": { + "defaultMessage": "主题" + }, + "course.forum.ForumTopicTable.unresolved": { + "defaultMessage": "问题(未解决)" + }, + "course.forum.ForumTopicTable.views": { + "defaultMessage": "观点" + }, + "course.forum.ForumTopicTable.votes": { + "defaultMessage": "点赞数" + }, + "course.forum.ForumsIndex.fetchForumsFailure": { + "defaultMessage": "无法检索论坛数据。" + }, + "course.forum.ForumsIndex.header": { + "defaultMessage": "论坛" + }, + "course.forum.ForumsIndex.markAllAsReadFailed": { + "defaultMessage": "无法将所有主题标记为已读。请稍后再试。" + }, + "course.forum.ForumsIndex.markAllAsReadSuccess": { + "defaultMessage": "所有主题都已标记为已读。" + }, + "course.forum.ForumsIndex.newForum": { + "defaultMessage": "新建分论坛" + }, + "course.forum.HideButton.hide": { + "defaultMessage": "隐藏" + }, + "course.forum.HideButton.hideFailure": { + "defaultMessage": "未能隐藏主题\"{title}\"- {error}" + }, + "course.forum.HideButton.hideSuccess": { + "defaultMessage": "主题\"{title}\"已成功隐藏。" + }, + "course.forum.HideButton.hideTooltip": { + "defaultMessage": "对学生隐藏主题" + }, + "course.forum.HideButton.unhide": { + "defaultMessage": "取消隐藏" + }, + "course.forum.HideButton.unhideFailure": { + "defaultMessage": "未能取消隐藏主题\"{title}\"- {error}" + }, + "course.forum.HideButton.unhideSuccess": { + "defaultMessage": "主题\"{title}\"已成功取消隐藏。" + }, + "course.forum.HideButton.unhideTooltip": { + "defaultMessage": "向学生显示主题" + }, + "course.forum.LockButton.lockTooltip": { + "defaultMessage": "锁定以禁止学生在此主题中发帖" + }, + "course.forum.LockButton.locked": { + "defaultMessage": "锁定" + }, + "course.forum.LockButton.lockedFailure": { + "defaultMessage": "无法锁定主题\"{title}\"- {error}" + }, + "course.forum.LockButton.lockedSuccess": { + "defaultMessage": "主题\"{title}\"已成功锁定。" + }, + "course.forum.LockButton.unlockTooltip": { + "defaultMessage": "解锁以允许学生在此主题中发帖" + }, + "course.forum.LockButton.unlocked": { + "defaultMessage": "解锁" + }, + "course.forum.LockButton.unlockedFailure": { + "defaultMessage": "无法解锁主题\"{title}\" - {error}" + }, + "course.forum.LockButton.unlockedSuccess": { + "defaultMessage": "主题\"{title}\"已成功解锁。" + }, + "course.forum.MarkAllAsReadButton.AllReadTooltip": { + "defaultMessage": "所有主题均已读" + }, + "course.forum.MarkAllAsReadButton.markAllAsRead": { + "defaultMessage": "标记为已读" + }, + "course.forum.MarkAllAsReadButton.markAllAsReadTooltip": { + "defaultMessage": "标记当前页面所有帖子为已读" + }, + "course.forum.MarkAnswerButton.markAsAnswer": { + "defaultMessage": "标记为答案" + }, + "course.forum.MarkAnswerButton.markedAsAnswer": { + "defaultMessage": "已标记为答案" + }, + "course.forum.MarkAnswerButton.unmarkAsAnswer": { + "defaultMessage": "取消标记为答案" + }, + "course.forum.MarkAnswerButton.updateFailure": { + "defaultMessage": "无法更新帖子 - {error}" + }, + "course.forum.NextUnreadButton.AllReadTooltip": { + "defaultMessage": "所有主题均已读" + }, + "course.forum.NextUnreadButton.nextUnread": { + "defaultMessage": "下一条未读" + }, + "course.forum.NextUnreadButton.nextUnreadTooltip": { + "defaultMessage": "跳转到下一个未读主题" + }, + "course.forum.PostCreatorObject.anonymousUser": { + "defaultMessage": "匿名用户" + }, + "course.forum.PostCreatorObject.maskUser": { + "defaultMessage": "标记用户" + }, + "course.forum.PostCreatorObject.postAnonymously": { + "defaultMessage": "匿名发帖" + }, + "course.forum.PostCreatorObject.unmaskUser": { + "defaultMessage": "取消屏蔽用户" + }, + "course.forum.ReplyCard.emptyPost": { + "defaultMessage": "内容不能为空" + }, + "course.forum.ReplyCard.postAnonymously": { + "defaultMessage": "匿名发布" + }, + "course.forum.ReplyCard.replyFailure": { + "defaultMessage": "无法提交帖子 - {error}" + }, + "course.forum.ReplyCard.replySuccess": { + "defaultMessage": "创建回帖失败" + }, + "course.forum.ReplyCard.replyTo": { + "defaultMessage": "回复给 {user}" + }, + "course.forum.SubscribeButton.commonTranslations.manageMySubscription": { + "defaultMessage": "管理我的订阅" + }, + "course.forum.SubscribeButton.commonTranslations.subscribe": { + "defaultMessage": "订阅" + }, + "course.forum.SubscribeButton.commonTranslations.unsubscribe": { + "defaultMessage": "取消订阅" + }, + "course.forum.SubscribeButton.commonTranslations.updateSubscriptionFailure": { + "defaultMessage": "更新订阅失败 - {error}" + }, + "course.forum.SubscribeButton.forumTopicTranslations.adminSettingSubscribed": { + "defaultMessage": "课程管理员禁用了论坛主题的订阅。" + }, + "course.forum.SubscribeButton.forumTopicTranslations.subscribeSuccess": { + "defaultMessage": "你已成功订阅论坛主题{title}。" + }, + "course.forum.SubscribeButton.forumTopicTranslations.subscribeTooltip": { + "defaultMessage": "订阅以在有人回复此论坛主题时接收电子邮件通知。" + }, + "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeSuccess": { + "defaultMessage": "你已成功取消订阅论坛主题 {title}。" + }, + "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeTooltip": { + "defaultMessage": "取消订阅电邮通知,不再接收论坛主题的回复。" + }, + "course.forum.SubscribeButton.forumTopicTranslations.userSettingSubscribed": { + "defaultMessage": "你已取消订阅本课程中论坛的“新帖子和回复”。请前往 {manageMySubscriptionLink} 启用它。" + }, + "course.forum.SubscribeButton.forumTranslations.adminSettingSubscribed": { + "defaultMessage": "课程管理员禁用了新论坛主题的订阅。" + }, + "course.forum.SubscribeButton.forumTranslations.subscribeSuccess": { + "defaultMessage": "你已成功订阅{title}。" + }, + "course.forum.SubscribeButton.forumTranslations.subscribeTooltip": { + "defaultMessage": "订阅以在创建新主题时接收电子邮件通知。" + }, + "course.forum.SubscribeButton.forumTranslations.unsubscribeSuccess": { + "defaultMessage": "你已成功取消订阅{title}。" + }, + "course.forum.SubscribeButton.forumTranslations.unsubscribeTooltip": { + "defaultMessage": "取消订阅电邮通知,不再接收新主题被创建的通知。" + }, + "course.forum.SubscribeButton.forumTranslations.userSettingSubscribed": { + "defaultMessage": "你已取消订阅本课程中论坛的“新主题”。请前往 {manageMySubscriptionLink} 启用它。" + }, + "course.forum.VotePostButton.updateFailure": { + "defaultMessage": "更新点赞数量失败 - {error}" + }, + "course.forum.forum.markAllAsReadFailed": { + "defaultMessage": "未能将此论坛中的所有主题标记为已读。请稍后再试。" + }, + "course.gradebook.AddExternalColumnPrompt.cancel": { + "defaultMessage": "取消" + }, + "course.gradebook.AddExternalColumnPrompt.create": { + "defaultMessage": "创建" + }, + "course.gradebook.AddExternalColumnPrompt.error": { + "defaultMessage": "无法创建外部评估。" }, - "course.forum.ForumTable.posts": { - "defaultMessage": "帖子" + "course.gradebook.AddExternalColumnPrompt.maxLabel": { + "defaultMessage": "最高分" }, - "course.forum.ForumTable.topics": { - "defaultMessage": "主题" + "course.gradebook.AddExternalColumnPrompt.nameLabel": { + "defaultMessage": "名称" }, - "course.forum.ForumTable.views": { - "defaultMessage": "阅读量" + "course.gradebook.AddExternalColumnPrompt.success": { + "defaultMessage": "外部评估已创建。" }, - "course.forum.ForumTable.votes": { - "defaultMessage": "点赞数" + "course.gradebook.AddExternalColumnPrompt.title": { + "defaultMessage": "添加外部评估" }, - "course.forum.ForumTopicEdit.editForum": { - "defaultMessage": "编辑主题" + "course.gradebook.ConfigureWeightsPrompt.allExcluded": { + "defaultMessage": "“{tab}”中的所有评估均已排除,且不会计入总成绩。" }, - "course.forum.ForumTopicEdit.updateFailure": { - "defaultMessage": "未能更新主题。" + "course.gradebook.ConfigureWeightsPrompt.allExcludedCount": { + "defaultMessage": "已排除全部 {n} 个" }, - "course.forum.ForumTopicEdit.updateSuccess": { - "defaultMessage": "主题 {title} 已更新。" + "course.gradebook.ConfigureWeightsPrompt.customMode": { + "defaultMessage": "自定义" }, - "course.forum.ForumTopicForm.postAnonymously": { - "defaultMessage": "匿名发帖" + "course.gradebook.ConfigureWeightsPrompt.customSum": { + "defaultMessage": "评估权重:{sum} / {total}" }, - "course.forum.ForumTopicForm.text": { - "defaultMessage": "文本" + "course.gradebook.ConfigureWeightsPrompt.defaultsHint": { + "defaultMessage": "尚未设置权重。以下为建议默认值,所有标签页均按相同比重计算。保存以确认,或在下方进行调整。" }, - "course.forum.ForumTopicForm.title": { - "defaultMessage": "标题" + "course.gradebook.ConfigureWeightsPrompt.descriptionDrop": { + "defaultMessage": "在等权模式下,可选择在求平均分前剔除每位学生得分最低的 N 个评估。" }, - "course.forum.ForumTopicForm.topicType": { - "defaultMessage": "主题类型" + "course.gradebook.ConfigureWeightsPrompt.descriptionExclusion": { + "defaultMessage": "展开标签页以将个别评估纳入或排除在评分之外。" }, - "course.forum.ForumTopicForm.topicType.announcement": { - "defaultMessage": "公告" + "course.gradebook.ConfigureWeightsPrompt.descriptionIntro": { + "defaultMessage": "控制各标签页和评估如何计入每位学生的总成绩。" }, - "course.forum.ForumTopicForm.topicType.normal": { - "defaultMessage": "普通" + "course.gradebook.ConfigureWeightsPrompt.descriptionModes": { + "defaultMessage": "选择“等权”(所有评估平分该标签页的权重)或“自定义”(设置每个评估所占的份额)。" }, - "course.forum.ForumTopicForm.topicType.question": { - "defaultMessage": "问题" + "course.gradebook.ConfigureWeightsPrompt.descriptionWeights": { + "defaultMessage": "设置每个标签页的权重,即其对总成绩的贡献比例(权重合计应为 100)。" }, - "course.forum.ForumTopicForm.topicType.sticky": { - "defaultMessage": "置顶" + "course.gradebook.ConfigureWeightsPrompt.equalMode": { + "defaultMessage": "等权" }, - "course.forum.ForumTopicManagementButtons.deletionConfirm": { - "defaultMessage": "你确定要删除此主题\"{title}\"吗?" + "course.gradebook.ConfigureWeightsPrompt.excluded": { + "defaultMessage": "已排除" }, - "course.forum.ForumTopicManagementButtons.deletionFailure": { - "defaultMessage": "无法删除主题 - {error}" + "course.gradebook.ConfigureWeightsPrompt.excludedCount": { + "defaultMessage": "已排除 {n} 个" }, - "course.forum.ForumTopicManagementButtons.deletionSuccess": { - "defaultMessage": "主题 {title} 已删除。" + "course.gradebook.ConfigureWeightsPrompt.includeAssessment": { + "defaultMessage": "将 {assessment} 计入成绩" }, - "course.forum.ForumTopicNew.creationFailure": { - "defaultMessage": "创建主题失败。" + "course.gradebook.ConfigureWeightsPrompt.modeAria": { + "defaultMessage": "{tab} 权重模式" }, - "course.forum.ForumTopicNew.creationSuccess": { - "defaultMessage": "主题 {title} 已创建。" + "course.gradebook.ConfigureWeightsPrompt.ofGrade": { + "defaultMessage": "占成绩的 {pct}%" }, - "course.forum.ForumTopicNew.newTopic": { - "defaultMessage": "新主题" + "course.gradebook.ConfigureWeightsPrompt.promptTitle": { + "defaultMessage": "配置贡献比例" }, - "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptAction": { - "defaultMessage": "放弃修改" + "course.gradebook.ConfigureWeightsPrompt.saveError": { + "defaultMessage": "保存权重失败。请重试。" }, - "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptMessage": { - "defaultMessage": "此文章已被编辑,你要放弃未保存的更改吗?" + "course.gradebook.ConfigureWeightsPrompt.total": { + "defaultMessage": "总计:{sum}%" }, - "course.forum.ForumTopicPostEditActionButtons.discardEditPostPromptTitle": { - "defaultMessage": "放弃未保存的更改吗?" + "course.gradebook.ConfigureWeightsPrompt.unbalanced": { + "defaultMessage": "保存前,“{tab}”的评估权重合计必须等于该标签页的总权重。" }, - "course.forum.ForumTopicPostEditActionButtons.emptyPost": { - "defaultMessage": "内容不能为空" + "course.gradebook.ConfigureWeightsPrompt.valueTooHigh": { + "defaultMessage": "数值最多为 100" }, - "course.forum.ForumTopicPostEditActionButtons.updateFailure": { - "defaultMessage": "无法更新帖子 - {error}" + "course.gradebook.ConfigureWeightsPrompt.valueTooLow": { + "defaultMessage": "数值至少为 0" }, - "course.forum.ForumTopicPostEditActionButtons.updateSuccess": { - "defaultMessage": "帖子已被更新" + "course.gradebook.ConfigureWeightsPrompt.weightsDoNotSum": { + "defaultMessage": "权重合计不为 100。仍可保存;总成绩可能不准确。" }, - "course.forum.ForumTopicPostForm.postAnonymously": { - "defaultMessage": "匿名发帖" + "course.gradebook.DeleteExternalColumnPrompt.body": { + "defaultMessage": "要删除\"{title}\"吗?这将永久移除该列及其中每位学生的成绩。此操作无法撤销。" }, - "course.forum.ForumTopicPostManagementButtons.deletionConfirm": { - "defaultMessage": "你确定要删除此主题帖吗?" + "course.gradebook.DeleteExternalColumnPrompt.cancel": { + "defaultMessage": "取消" }, - "course.forum.ForumTopicPostManagementButtons.deletionFailure": { - "defaultMessage": "无法删除主题 - {error}" + "course.gradebook.DeleteExternalColumnPrompt.confirm": { + "defaultMessage": "删除" }, - "course.forum.ForumTopicPostManagementButtons.deletionSuccess": { - "defaultMessage": "该帖子已被删除。" + "course.gradebook.DeleteExternalColumnPrompt.error": { + "defaultMessage": "无法删除外部评估。" }, - "course.forum.ForumTopicPostNew.creationFailure": { - "defaultMessage": "无法创建帖子 - {error}" + "course.gradebook.DeleteExternalColumnPrompt.title": { + "defaultMessage": "删除外部评估" }, - "course.forum.ForumTopicPostNew.creationSuccess": { - "defaultMessage": "帖子已创建。" + "course.gradebook.ExternalGradeConflictPrompt.body": { + "defaultMessage": "这些学生在这些组成部分中已有成绩。要保留他们现有的成绩,还是用文件中的值替换?新学生和空白单元格不受影响。" }, - "course.forum.ForumTopicPostNew.newPost": { - "defaultMessage": "创建新帖子" + "course.gradebook.ExternalGradeConflictPrompt.goBack": { + "defaultMessage": "返回" }, - "course.forum.ForumTopicShow.fetchPostsFailure": { - "defaultMessage": "无法检索论坛主题数据。" + "course.gradebook.ExternalGradeConflictPrompt.keepExisting": { + "defaultMessage": "保留现有成绩" }, - "course.forum.ForumTopicShow.header": { - "defaultMessage": "论坛主题帖" + "course.gradebook.ExternalGradeConflictPrompt.replace": { + "defaultMessage": "替换" }, - "course.forum.ForumTopicShow.lockedNote": { - "defaultMessage": "你无法添加新帖子,因为该主题已被教学人员锁定。" + "course.gradebook.ExternalGradeConflictPrompt.title": { + "defaultMessage": "解决成绩冲突" }, - "course.forum.ForumTopicShow.noPosts": { - "defaultMessage": "没有帖子" + "course.gradebook.ExternalGradeConflictTable.component": { + "defaultMessage": "组成部分" }, - "course.forum.ForumTopicShow.topicResolved": { - "defaultMessage": "这个问题已经解决。" + "course.gradebook.ExternalGradeConflictTable.existing": { + "defaultMessage": "现有成绩" }, - "course.forum.ForumTopicShow.topicUnresolved": { - "defaultMessage": "此问题未解决。" + "course.gradebook.ExternalGradeConflictTable.inFile": { + "defaultMessage": "文件中的成绩" }, - "course.forum.ForumTopicShow.topicUnresolvedNote": { - "defaultMessage": "将有帮助的帖子标记为答案以解决此问题。" + "course.gradebook.ExternalGradeConflictTable.mismatch": { + "defaultMessage": "此标识符现在对应的学生与现有成绩导入时对应的学生不同。" }, - "course.forum.ForumTopicTable.hidden": { - "defaultMessage": "该主题对学生是隐藏的。" + "course.gradebook.ExternalGradeConflictTable.student": { + "defaultMessage": "学生" }, - "course.forum.ForumTopicTable.isSubscribed": { - "defaultMessage": "已订阅?" + "course.gradebook.GradeLinkHint.hint": { + "defaultMessage": "每个成绩都是学生提交内容中各项分数的总和。点击任一成绩即可打开该提交内容并调整分数。" }, - "course.forum.ForumTopicTable.lastPostedBy": { - "defaultMessage": "最后发表者" + "course.gradebook.GradebookColumnTree.alwaysIncluded": { + "defaultMessage": "必选项" }, - "course.forum.ForumTopicTable.locked": { - "defaultMessage": "该主题已关闭;它不再接受新的回复。" + "course.gradebook.GradebookColumnTree.gamification": { + "defaultMessage": "游戏化相关" }, - "course.forum.ForumTopicTable.noTopic": { - "defaultMessage": "没有主题" + "course.gradebook.GradebookColumnTree.grades": { + "defaultMessage": "成绩" }, - "course.forum.ForumTopicTable.posts": { - "defaultMessage": "帖子" + "course.gradebook.GradebookColumnTree.studentInfo": { + "defaultMessage": "学生信息" }, - "course.forum.ForumTopicTable.resolved": { - "defaultMessage": "问题(已解决)" + "course.gradebook.GradebookIndex.addExternal": { + "defaultMessage": "添加外部评估" }, - "course.forum.ForumTopicTable.startedBy": { - "defaultMessage": "开始于" + "course.gradebook.GradebookIndex.allAssessments": { + "defaultMessage": "全部评估" }, - "course.forum.ForumTopicTable.topics": { - "defaultMessage": "主题" + "course.gradebook.GradebookIndex.applyAndExport": { + "defaultMessage": "应用并导出" }, - "course.forum.ForumTopicTable.unresolved": { - "defaultMessage": "问题(未解决)" + "course.gradebook.GradebookIndex.byWeight": { + "defaultMessage": "加权总成绩" }, - "course.forum.ForumTopicTable.views": { - "defaultMessage": "观点" + "course.gradebook.GradebookIndex.dialogTitle": { + "defaultMessage": "选择列" }, - "course.forum.ForumTopicTable.votes": { - "defaultMessage": "点赞数" + "course.gradebook.GradebookIndex.exportAllTooltip": { + "defaultMessage": "未选择行 - 将导出所有行。" }, - "course.forum.ForumsIndex.fetchForumsFailure": { - "defaultMessage": "无法检索论坛数据。" + "course.gradebook.GradebookIndex.exportButton": { + "defaultMessage": "导出所有行" }, - "course.forum.ForumsIndex.header": { - "defaultMessage": "论坛" + "course.gradebook.GradebookIndex.exportRows": { + "defaultMessage": "导出 {count, plural, other {# 行}}" }, - "course.forum.ForumsIndex.markAllAsReadFailed": { - "defaultMessage": "无法将所有主题标记为已读。请稍后再试。" + "course.gradebook.GradebookIndex.fetchFailure": { + "defaultMessage": "无法获取成绩册。" }, - "course.forum.ForumsIndex.markAllAsReadSuccess": { - "defaultMessage": "所有主题都已标记为已读。" + "course.gradebook.GradebookIndex.gradebook": { + "defaultMessage": "成绩册" }, - "course.forum.ForumsIndex.newForum": { - "defaultMessage": "新建分论坛" + "course.gradebook.GradebookIndex.noStudents": { + "defaultMessage": "无学生" }, - "course.forum.HideButton.hide": { - "defaultMessage": "隐藏" + "course.gradebook.GradebookIndex.noStudentsHint": { + "defaultMessage": "学生加入课程后,成绩将显示在这里。" }, - "course.forum.HideButton.hideTooltip": { - "defaultMessage": "对学生隐藏主题" + "course.gradebook.GradebookIndex.searchStudents": { + "defaultMessage": "搜索学生" }, - "course.forum.HideButton.hideFailure": { - "defaultMessage": "未能隐藏主题\"{title}\"- {error}" + "course.gradebook.GradebookIndex.selectColumns": { + "defaultMessage": "选择列" }, - "course.forum.HideButton.hideSuccess": { - "defaultMessage": "主题\"{title}\"已成功隐藏。" + "course.gradebook.GradebookTable.delete": { + "defaultMessage": "删除" }, - "course.forum.HideButton.unhide": { - "defaultMessage": "取消隐藏" + "course.gradebook.GradebookTable.externalBadge": { + "defaultMessage": "外部" }, - "course.forum.HideButton.unhideTooltip": { - "defaultMessage": "向学生显示主题" + "course.gradebook.GradebookTable.externalGradeAria": { + "defaultMessage": "{name} 的 {title} 成绩" }, - "course.forum.HideButton.unhideFailure": { - "defaultMessage": "未能取消隐藏主题\"{title}\"- {error}" + "course.gradebook.GradebookTable.externalMaxAria": { + "defaultMessage": "{title} 最高分" }, - "course.forum.HideButton.unhideSuccess": { - "defaultMessage": "主题\"{title}\"已成功取消隐藏。" + "course.gradebook.GradebookTable.gradeSaveError": { + "defaultMessage": "无法保存成绩。请重试。" }, - "course.forum.LockButton.locked": { - "defaultMessage": "锁定" + "course.gradebook.GradebookTable.manageAria": { + "defaultMessage": "管理 {title}" }, - "course.forum.LockButton.lockTooltip": { - "defaultMessage": "锁定以禁止学生在此主题中发帖" + "course.gradebook.GradebookTable.maxMarks": { + "defaultMessage": "最高分" }, - "course.forum.LockButton.lockedFailure": { - "defaultMessage": "无法锁定主题\"{title}\"- {error}" + "course.gradebook.GradebookTable.maxSaveError": { + "defaultMessage": "无法保存最高分。请重试。" }, - "course.forum.LockButton.lockedSuccess": { - "defaultMessage": "主题\"{title}\"已成功锁定。" + "course.gradebook.GradebookTable.noDataColumnsHint": { + "defaultMessage": "未选择任何成绩列--导出内容将仅包含学生信息。" }, - "course.forum.LockButton.unlocked": { - "defaultMessage": "解锁" + "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { + "defaultMessage": "未选择任何成绩或游戏化列--导出内容将仅包含学生信息。" }, - "course.forum.LockButton.unlockTooltip": { - "defaultMessage": "解锁以允许学生在此主题中发帖" + "course.gradebook.GradebookTable.rename": { + "defaultMessage": "重命名" }, - "course.forum.LockButton.unlockedFailure": { - "defaultMessage": "无法解锁主题\"{title}\" - {error}" + "course.gradebook.GradebookWeightedTable.collapseRow": { + "defaultMessage": "收起 {name}" }, - "course.forum.LockButton.unlockedSuccess": { - "defaultMessage": "主题\"{title}\"已成功解锁。" + "course.gradebook.GradebookWeightedTable.configureWeights": { + "defaultMessage": "配置权重" }, - "course.forum.MarkAllAsReadButton.AllReadTooltip": { - "defaultMessage": "所有主题均已读" + "course.gradebook.GradebookWeightedTable.defaultWeights": { + "defaultMessage": "正在显示默认权重--所有标签页比重相同。点击“配置权重”进行自定义设置。" }, - "course.forum.MarkAllAsReadButton.markAllAsRead": { - "defaultMessage": "标记为已读" + "course.gradebook.GradebookWeightedTable.defaultWeightsNoAccess": { + "defaultMessage": "正在显示默认权重--在配置权重之前,所有标签页比重相同。" }, - "course.forum.MarkAllAsReadButton.markAllAsReadTooltip": { - "defaultMessage": "标记当前页面所有帖子为已读" + "course.gradebook.GradebookWeightedTable.displayMode": { + "defaultMessage": "显示模式" + }, + "course.gradebook.GradebookWeightedTable.displayPercent": { + "defaultMessage": "百分比" }, - "course.forum.MarkAnswerButton.markAsAnswer": { - "defaultMessage": "标记为答案" + "course.gradebook.GradebookWeightedTable.displayPercentTooltip": { + "defaultMessage": "学生在各标签页所获得的比例。在比重为 20% 的标签页获得 100%,即表示学生获得了该标签页的全部 20 个成绩分。" }, - "course.forum.MarkAnswerButton.markedAsAnswer": { - "defaultMessage": "已标记为答案" + "course.gradebook.GradebookWeightedTable.displayPoints": { + "defaultMessage": "分数" }, - "course.forum.MarkAnswerButton.unmarkAsAnswer": { - "defaultMessage": "取消标记为答案" + "course.gradebook.GradebookWeightedTable.displayPointsTooltip": { + "defaultMessage": "各标签页贡献的成绩分数。各列相加即为预计总成绩。" }, - "course.forum.MarkAnswerButton.updateFailure": { - "defaultMessage": "无法更新帖子 - {error}" + "course.gradebook.GradebookWeightedTable.downloadCsv": { + "defaultMessage": "下载为 CSV" }, - "course.forum.NextUnreadButton.AllReadTooltip": { - "defaultMessage": "所有主题均已读" + "course.gradebook.GradebookWeightedTable.email": { + "defaultMessage": "电子邮件" }, - "course.forum.NextUnreadButton.nextUnread": { - "defaultMessage": "下一条未读" + "course.gradebook.GradebookWeightedTable.excluded": { + "defaultMessage": "已排除" }, - "course.forum.NextUnreadButton.nextUnreadTooltip": { - "defaultMessage": "跳转到下一个未读主题" + "course.gradebook.GradebookWeightedTable.expandRow": { + "defaultMessage": "展开 {name}" }, - "course.forum.PostCreatorObject.anonymousUser": { - "defaultMessage": "匿名用户" + "course.gradebook.GradebookWeightedTable.name": { + "defaultMessage": "姓名" }, - "course.forum.PostCreatorObject.maskUser": { - "defaultMessage": "标记用户" + "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { + "defaultMessage": "尚未配置权重--所有标签页的权重均为 0。点击“配置权重”以分配权重。" }, - "course.forum.PostCreatorObject.postAnonymously": { - "defaultMessage": "匿名发帖" + "course.gradebook.GradebookWeightedTable.noWeightsNoAccess": { + "defaultMessage": "尚未配置任何标签页权重。" }, - "course.forum.PostCreatorObject.unmaskUser": { - "defaultMessage": "取消屏蔽用户" + "course.gradebook.GradebookWeightedTable.outOfWeight": { + "defaultMessage": "/{weight}" }, - "course.forum.ReplyCard.emptyPost": { - "defaultMessage": "内容不能为空" + "course.gradebook.GradebookWeightedTable.percentOfGrade": { + "defaultMessage": "占成绩的 {weight}%" }, - "course.forum.ReplyCard.postAnonymously": { - "defaultMessage": "匿名发布" + "course.gradebook.GradebookWeightedTable.percentTotalExact": { + "defaultMessage": "合计 100%" }, - "course.forum.ReplyCard.replyFailure": { - "defaultMessage": "无法提交帖子 - {error}" + "course.gradebook.GradebookWeightedTable.percentTotalWarning": { + "defaultMessage": "合计 {weight}%" }, - "course.forum.ReplyCard.replySuccess": { - "defaultMessage": "创建回帖失败" + "course.gradebook.GradebookWeightedTable.searchStudents": { + "defaultMessage": "搜索学生" }, - "course.forum.ReplyCard.replyTo": { - "defaultMessage": "回复给 {user}" + "course.gradebook.GradebookWeightedTable.total": { + "defaultMessage": "总成绩" }, - "course.forum.SubscribeButton.commonTranslations.manageMySubscription": { - "defaultMessage": "管理我的订阅" + "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { + "defaultMessage": "权重合计不为 100。总成绩可能不准确。" }, - "course.forum.SubscribeButton.commonTranslations.subscribe": { - "defaultMessage": "订阅" + "course.gradebook.ImportExternalAssessmentsButton.label": { + "defaultMessage": "导入外部评估" }, - "course.forum.SubscribeButton.commonTranslations.unsubscribe": { - "defaultMessage": "取消订阅" + "course.gradebook.ImportWizard.addComponent": { + "defaultMessage": "添加组成部分" }, - "course.forum.SubscribeButton.commonTranslations.updateSubscriptionFailure": { - "defaultMessage": "更新订阅失败 - {error}" + "course.gradebook.ImportWizard.back": { + "defaultMessage": "返回" }, - "course.forum.SubscribeButton.forumTopicTranslations.adminSettingSubscribed": { - "defaultMessage": "课程管理员禁用了论坛主题的订阅。" + "course.gradebook.ImportWizard.cancel": { + "defaultMessage": "取消" }, - "course.forum.SubscribeButton.forumTopicTranslations.subscribeSuccess": { - "defaultMessage": "你已成功订阅论坛主题{title}。" + "course.gradebook.ImportWizard.commitError": { + "defaultMessage": "导入失败。未保存任何内容。" }, - "course.forum.SubscribeButton.forumTopicTranslations.subscribeTooltip": { - "defaultMessage": "订阅以在有人回复此论坛主题时接收电子邮件通知。" + "course.gradebook.ImportWizard.committed": { + "defaultMessage": "导入完成。" }, - "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeSuccess": { - "defaultMessage": "你已成功取消订阅论坛主题 {title}。" + "course.gradebook.ImportWizard.componentName": { + "defaultMessage": "组成部分名称" }, - "course.forum.SubscribeButton.forumTopicTranslations.unsubscribeTooltip": { - "defaultMessage": "取消订阅电邮通知,不再接收论坛主题的回复。" + "course.gradebook.ImportWizard.continue": { + "defaultMessage": "确认导入" }, - "course.forum.SubscribeButton.forumTopicTranslations.userSettingSubscribed": { - "defaultMessage": "你已取消订阅本课程中论坛的“新帖子和回复”。请前往 {manageMySubscriptionLink} 启用它。" + "course.gradebook.ImportWizard.downloadTemplate": { + "defaultMessage": "下载模板" }, - "course.forum.SubscribeButton.forumTranslations.adminSettingSubscribed": { - "defaultMessage": "课程管理员禁用了新论坛主题的订阅。" + "course.gradebook.ImportWizard.email": { + "defaultMessage": "电子邮件" }, - "course.forum.SubscribeButton.forumTranslations.subscribeSuccess": { - "defaultMessage": "你已成功订阅{title}。" + "course.gradebook.ImportWizard.identifierMode": { + "defaultMessage": "匹配学生的依据" }, - "course.forum.SubscribeButton.forumTranslations.subscribeTooltip": { - "defaultMessage": "订阅以在创建新主题时接收电子邮件通知。" + "course.gradebook.ImportWizard.malformed": { + "defaultMessage": "以下单元格不是有效数字:{cells}" }, - "course.forum.SubscribeButton.forumTranslations.unsubscribeSuccess": { - "defaultMessage": "你已成功取消订阅{title}。" + "course.gradebook.ImportWizard.maxMarks": { + "defaultMessage": "最高分" }, - "course.forum.SubscribeButton.forumTranslations.unsubscribeTooltip": { - "defaultMessage": "取消订阅电邮通知,不再接收新主题被创建的通知。" + "course.gradebook.ImportWizard.next": { + "defaultMessage": "下一步" }, - "course.forum.SubscribeButton.forumTranslations.userSettingSubscribed": { - "defaultMessage": "你已取消订阅本课程中论坛的“新主题”。请前往 {manageMySubscriptionLink} 启用它。" + "course.gradebook.ImportWizard.previewError": { + "defaultMessage": "无法验证文件。请重试。" }, - "course.forum.VotePostButton.updateFailure": { - "defaultMessage": "更新点赞数量失败 - {error}" + "course.gradebook.ImportWizard.stepDefine": { + "defaultMessage": "定义组成部分" }, - "course.forum.forum.markAllAsReadFailed": { - "defaultMessage": "未能将此论坛中的所有主题标记为已读。请稍后再试。" + "course.gradebook.ImportWizard.stepUpload": { + "defaultMessage": "模板和上传" }, - "course.gradebook.GradebookColumnTree.grades": { - "defaultMessage": "成绩" + "course.gradebook.ImportWizard.stepVerify": { + "defaultMessage": "验证" }, - "course.gradebook.GradebookColumnTree.studentInfo": { - "defaultMessage": "学生信息" + "course.gradebook.ImportWizard.studentId": { + "defaultMessage": "学生 ID" }, - "course.gradebook.GradebookColumnTree.alwaysIncluded": { - "defaultMessage": "必选项" + "course.gradebook.ImportWizard.studentIdHint": { + "defaultMessage": "匹配会使用每位学生当前的学生 ID。请在\"管理用户\"中保持学生 ID 为最新。" }, - "course.gradebook.GradebookColumnTree.gamification": { - "defaultMessage": "游戏化相关" + "course.gradebook.ImportWizard.title": { + "defaultMessage": "导入外部评估" }, - "course.gradebook.GradebookIndex.dialogTitle": { - "defaultMessage": "选择列" + "course.gradebook.ImportWizard.unresolved": { + "defaultMessage": "在课程中找不到以下标识符:{ids}" }, - "course.gradebook.GradebookIndex.exportAllTooltip": { - "defaultMessage": "未选择行 - 将导出所有行。" + "course.gradebook.ImportWizard.updatesExisting": { + "defaultMessage": "更新现有项目 - 在成绩册中管理" }, - "course.gradebook.GradebookIndex.exportButton": { - "defaultMessage": "导出所有行" + "course.gradebook.ImportWizard.upload": { + "defaultMessage": "上传已填写的 CSV" }, - "course.gradebook.GradebookIndex.exportRows": { - "defaultMessage": "导出 {count, plural, other {# 行}}" + "course.gradebook.ImportWizard.verify": { + "defaultMessage": "验证" }, - "course.gradebook.GradebookIndex.selectColumns": { - "defaultMessage": "选择列" + "course.gradebook.ImportWizard.weightage": { + "defaultMessage": "权重" }, - "course.gradebook.GradebookIndex.applyAndExport": { - "defaultMessage": "应用并导出" + "course.gradebook.ProjectedTotalHint.policy": { + "defaultMessage": "总分会将未评分的评估计为 0。" }, - "course.gradebook.GradebookIndex.fetchFailure": { - "defaultMessage": "无法获取成绩册。" + "course.gradebook.RenameExternalColumnPrompt.cancel": { + "defaultMessage": "取消" }, - "course.gradebook.GradebookIndex.gradebook": { - "defaultMessage": "成绩册" + "course.gradebook.RenameExternalColumnPrompt.error": { + "defaultMessage": "无法重命名外部评估。" }, - "course.gradebook.GradebookIndex.searchStudents": { - "defaultMessage": "搜索学生" + "course.gradebook.RenameExternalColumnPrompt.nameLabel": { + "defaultMessage": "名称" }, - "course.gradebook.GradebookIndex.noStudents": { - "defaultMessage": "无学生" + "course.gradebook.RenameExternalColumnPrompt.save": { + "defaultMessage": "保存" }, - "course.gradebook.GradebookTable.maxMarks": { - "defaultMessage": "最高分" + "course.gradebook.RenameExternalColumnPrompt.title": { + "defaultMessage": "重命名外部评估" }, - "course.gradebook.GradebookTable.noDataColumnsHint": { - "defaultMessage": "未选择任何成绩列——导出内容将仅包含学生信息。" + "course.gradebook.TotalHint.policy": { + "defaultMessage": "总成绩将未评分的评估按 0 分计算。" }, - "course.gradebook.GradebookTable.noDataColumnsHintWithGamification": { - "defaultMessage": "未选择任何成绩或游戏化列——导出内容将仅包含学生信息。" + "course.gradebook.WeightedViewHint.hint": { + "defaultMessage": "需要加权总成绩吗?您可以设置每个标签页在每位学生总成绩中所占的比重,并在此查看加权总成绩。请在{link}中启用。" + }, + "course.gradebook.WeightedViewHint.settingsLink": { + "defaultMessage": "成绩册设置" }, "course.group.GroupCreationForm.description": { "defaultMessage": "说明(可选)" @@ -5675,27 +6023,27 @@ "course.leaderboard.LeaderboardTable.average": { "defaultMessage": "平均的" }, - "course.leaderboard.LeaderboardTable.experience": { - "defaultMessage": "经验" + "course.leaderboard.LeaderboardTable.averageAchievements": { + "defaultMessage": "平均成就" }, - "course.leaderboard.LeaderboardTable.rank": { - "defaultMessage": "排名" + "course.leaderboard.LeaderboardTable.averageExperience": { + "defaultMessage": "平均经验值" }, - "course.leaderboard.LeaderboardTable.name": { - "defaultMessage": "姓名" + "course.leaderboard.LeaderboardTable.experience": { + "defaultMessage": "经验" }, "course.leaderboard.LeaderboardTable.level": { "defaultMessage": "级别" }, - "course.leaderboard.LeaderboardTable.averageExperience": { - "defaultMessage": "平均经验值" - }, - "course.leaderboard.LeaderboardTable.averageAchievements": { - "defaultMessage": "平均成就" - }, "course.leaderboard.LeaderboardTable.members": { "defaultMessage": "成员" }, + "course.leaderboard.LeaderboardTable.name": { + "defaultMessage": "姓名" + }, + "course.leaderboard.LeaderboardTable.rank": { + "defaultMessage": "排名" + }, "course.leaderboard.LeaderboardTable.titleAchievements": { "defaultMessage": "按成就" }, @@ -5972,6 +6320,15 @@ "course.material.folders.UploadFilesButton.uploadFilesTooltip": { "defaultMessage": "上传" }, + "course.material.folders.WorkbinTable.lastModified": { + "defaultMessage": "最后修改" + }, + "course.material.folders.WorkbinTable.name": { + "defaultMessage": "名称" + }, + "course.material.folders.WorkbinTable.startAt": { + "defaultMessage": "开始于" + }, "course.material.folders.WorkbinTableButtons.DeletionFailure": { "defaultMessage": "无法删除" }, @@ -5984,86 +6341,143 @@ "course.material.folders.WorkbinTableButtons.tableButtonDeleteTooltip": { "defaultMessage": "删除" }, - "course.material.folders.WorkbinTable.name": { - "defaultMessage": "名称" + "course.plagiarism.PlagiarismIndex.assessments.actions": { + "defaultMessage": "操作" }, - "course.material.folders.WorkbinTable.lastModified": { - "defaultMessage": "最后修改" + "course.plagiarism.PlagiarismIndex.assessments.assessment": { + "defaultMessage": "测验" }, - "course.material.folders.WorkbinTable.startAt": { - "defaultMessage": "开始于" + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { + "defaultMessage": "某些选定的测验已经完成了抄袭检查。运行新的抄袭检查将删除之前的结果。" }, - "course.plagiarism.PlagiarismIndex.header.plagiarism": { - "defaultMessage": "抄袭检查" + "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { + "defaultMessage": "确认抄袭检查?" }, - "course.plagiarism.PlagiarismIndex.assessments.assessment": { - "defaultMessage": "测验" + "course.plagiarism.PlagiarismIndex.assessments.lastRunStatus": { + "defaultMessage": "状态" + }, + "course.plagiarism.PlagiarismIndex.assessments.lastRunTime": { + "defaultMessage": "最后运行于" + }, + "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { + "defaultMessage": "最后提交于" + }, + "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { + "defaultMessage": "自上次抄袭检查以来检测到新提交" + }, + "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { + "defaultMessage": "自上次抄袭检查以来没有新提交" + }, + "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { + "defaultMessage": "没有可检查的问题" + }, + "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { + "defaultMessage": "提交数量不足" + }, + "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { + "defaultMessage": "# 可检查问题" }, "course.plagiarism.PlagiarismIndex.assessments.numSubmitted": { "defaultMessage": "# 提交" }, - "course.plagiarism.PlagiarismIndex.assessments.numCheckableQuestions": { - "defaultMessage": "# 可检查问题" + "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { + "defaultMessage": "新的抄袭检查 ({count})" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { + "defaultMessage": "运行抄袭检查" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { + "defaultMessage": "无法为某些测验开始抄袭检查" + }, + "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { + "defaultMessage": "已开始 {count, plural, =1 {# 个测验} other {# 个测验}} 的抄袭检查" + }, + "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { + "defaultMessage": "按测验标题搜索" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { + "defaultMessage": "已完成" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { + "defaultMessage": "失败" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { + "defaultMessage": "未开始" + }, + "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { + "defaultMessage": "运行中" + }, + "course.plagiarism.PlagiarismIndex.assessments.viewResults": { + "defaultMessage": "查看结果" + }, + "course.plagiarism.PlagiarismIndex.header.plagiarism": { + "defaultMessage": "抄袭检查" + }, + "course.statistics.StatisticsIndex.assessments.averageGrade": { + "defaultMessage": "平均成绩" + }, + "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { + "defaultMessage": "平均时间" }, - "course.plagiarism.PlagiarismIndex.assessments.lastSubmittedAt": { - "defaultMessage": "最后提交于" + "course.statistics.StatisticsIndex.assessments.category": { + "defaultMessage": "类别" }, - "course.plagiarism.PlagiarismIndex.assessments.lastRunStatus": { - "defaultMessage": "状态" + "course.statistics.StatisticsIndex.assessments.csvFileTitle": { + "defaultMessage": "测验统计" }, - "course.plagiarism.PlagiarismIndex.assessments.lastRunTime": { - "defaultMessage": "最后运行于" + "course.statistics.StatisticsIndex.assessments.downloadCsv": { + "defaultMessage": "下载" }, - "course.plagiarism.PlagiarismIndex.assessments.statusNotStarted": { - "defaultMessage": "未开始" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummary": { + "defaultMessage": "下载以下测验的成绩摘要?" }, - "course.plagiarism.PlagiarismIndex.assessments.statusRunning": { - "defaultMessage": "运行中" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { + "defaultMessage": "下载成绩摘要时出错" }, - "course.plagiarism.PlagiarismIndex.assessments.statusCompleted": { - "defaultMessage": "已完成" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { + "defaultMessage": "正在处理您的下载。请稍候。" }, - "course.plagiarism.PlagiarismIndex.assessments.statusFailed": { - "defaultMessage": "失败" + "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { + "defaultMessage": "成功下载成绩摘要" }, - "course.plagiarism.PlagiarismIndex.assessments.noPlagiarismCheckableQuestions": { - "defaultMessage": "没有可检查的问题" + "course.statistics.StatisticsIndex.assessments.numAttemptedStudents": { + "defaultMessage": "尝试人数" }, - "course.plagiarism.PlagiarismIndex.assessments.notEnoughSubmissions": { - "defaultMessage": "提交数量不足" + "course.statistics.StatisticsIndex.assessments.numLateStudents": { + "defaultMessage": "迟交人数" }, - "course.plagiarism.PlagiarismIndex.assessments.runAssessmentsPlagiarism": { - "defaultMessage": "新的抄袭检查 ({count})" + "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { + "defaultMessage": "已提交人数" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckSuccess": { - "defaultMessage": "已开始 {count, plural, =1 {# 个测验} other {# 个测验}} 的抄袭检查" + "course.statistics.StatisticsIndex.assessments.searchBar": { + "defaultMessage": "按测验标题、选项卡或类别搜索" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheckError": { - "defaultMessage": "无法为某些测验开始抄袭检查" + "course.statistics.StatisticsIndex.assessments.selectedNUsers": { + "defaultMessage": "下载 {numUsers} 个学生的成绩摘要?" }, - "course.plagiarism.PlagiarismIndex.assessments.searchByAssessmentTitle": { - "defaultMessage": "按测验标题搜索" + "course.statistics.StatisticsIndex.assessments.startAt": { + "defaultMessage": "开始于" }, - "course.plagiarism.PlagiarismIndex.assessments.actions": { - "defaultMessage": "操作" + "course.statistics.StatisticsIndex.assessments.stdevGrade": { + "defaultMessage": "成绩标准差" }, - "course.plagiarism.PlagiarismIndex.assessments.runPlagiarismCheck": { - "defaultMessage": "运行抄袭检查" + "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { + "defaultMessage": "时间标准差" }, - "course.plagiarism.PlagiarismIndex.assessments.viewResults": { - "defaultMessage": "查看结果" + "course.statistics.StatisticsIndex.assessments.subtitle": { + "defaultMessage": "若要查看和导出单个学生的成绩,请打开成绩册。" }, - "course.plagiarism.PlagiarismIndex.assessments.newSubmissionsWarning": { - "defaultMessage": "自上次抄袭检查以来检测到新提交" + "course.statistics.StatisticsIndex.assessments.subtitleDisabled": { + "defaultMessage": "要查看和导出个别学生的成绩,请启用 成绩册。" }, - "course.plagiarism.PlagiarismIndex.assessments.noNewSubmissionsWarning": { - "defaultMessage": "自上次抄袭检查以来没有新提交" + "course.statistics.StatisticsIndex.assessments.tab": { + "defaultMessage": "选项卡" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunTitle": { - "defaultMessage": "确认抄袭检查?" + "course.statistics.StatisticsIndex.assessments.tableTitle": { + "defaultMessage": "测验统计({numStudents} 个学生)" }, - "course.plagiarism.PlagiarismIndex.assessments.confirmRerunMessage": { - "defaultMessage": "某些选定的测验已经完成了抄袭检查。运行新的抄袭检查将删除之前的结果。" + "course.statistics.StatisticsIndex.assessments.title": { + "defaultMessage": "标题" }, "course.statistics.StatisticsIndex.course.StudentPerformanceTable.achievementCount": { "defaultMessage": "成就数(总计:{courseAchievementCount})" @@ -6140,9 +6554,6 @@ "course.statistics.StatisticsIndex.course.StudentPerformanceTable.videoSubmissionCountHeader": { "defaultMessage": "已观看视频 (总计: {courseVideoCount})" }, - "course.statistics.StatisticsIndex.course.searchBar": { - "defaultMessage": "按学生姓名搜索" - }, "course.statistics.StatisticsIndex.course.StudentProgressionChart.deadlines": { "defaultMessage": "截止日期" }, @@ -6182,78 +6593,18 @@ "course.statistics.StatisticsIndex.course.progressionError": { "defaultMessage": "获取课程进度统计时出了点问题! 请刷新重试。" }, + "course.statistics.StatisticsIndex.course.searchBar": { + "defaultMessage": "按学生姓名搜索" + }, "course.statistics.StatisticsIndex.header.statistics": { "defaultMessage": "数据" }, - "course.statistics.StatisticsIndex.assessments.title": { - "defaultMessage": "标题" - }, - "course.statistics.StatisticsIndex.assessments.startAt": { - "defaultMessage": "开始于" - }, - "course.statistics.StatisticsIndex.assessments.tab": { - "defaultMessage": "选项卡" - }, - "course.statistics.StatisticsIndex.assessments.category": { - "defaultMessage": "类别" - }, - "course.statistics.StatisticsIndex.assessments.numSubmittedStudents": { - "defaultMessage": "已提交人数" - }, - "course.statistics.StatisticsIndex.assessments.numAttemptedStudents": { - "defaultMessage": "尝试人数" - }, - "course.statistics.StatisticsIndex.assessments.numLateStudents": { - "defaultMessage": "迟交人数" - }, - "course.statistics.StatisticsIndex.assessments.averageGrade": { - "defaultMessage": "平均成绩" - }, - "course.statistics.StatisticsIndex.assessments.stdevGrade": { - "defaultMessage": "成绩标准差" - }, - "course.statistics.StatisticsIndex.assessments.averageTimeTaken": { - "defaultMessage": "平均时间" - }, - "course.statistics.StatisticsIndex.assessments.stdevTimeTaken": { - "defaultMessage": "时间标准差" - }, - "course.statistics.StatisticsIndex.assessments.tableTitle": { - "defaultMessage": "测验统计({numStudents} 个学生)" - }, - "course.statistics.StatisticsIndex.assessments.subtitle": { - "defaultMessage": "若要查看和导出单个学生的成绩,请打开成绩册。" - }, - "course.statistics.StatisticsIndex.assessments.subtitleDisabled": { - "defaultMessage": "要查看和导出个别学生的成绩,请启用 成绩册。" - }, - "course.statistics.StatisticsIndex.assessments.csvFileTitle": { - "defaultMessage": "测验统计" - }, - "course.statistics.StatisticsIndex.assessments.searchBar": { - "defaultMessage": "按测验标题、选项卡或类别搜索" - }, - "course.statistics.StatisticsIndex.assessments.selectedNUsers": { - "defaultMessage": "下载 {numUsers} 个学生的成绩摘要?" - }, - "course.statistics.StatisticsIndex.assessments.downloadCsv": { - "defaultMessage": "下载" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummary": { - "defaultMessage": "下载以下测验的成绩摘要?" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummarySuccess": { - "defaultMessage": "成功下载成绩摘要" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryFailure": { - "defaultMessage": "下载成绩摘要时出错" - }, - "course.statistics.StatisticsIndex.assessments.downloadScoreSummaryPending": { - "defaultMessage": "正在处理您的下载。请稍候。" - }, "course.statistics.StatisticsIndex.staff.averageMarkingTime": { "defaultMessage": "平均时间/测验" }, + "course.statistics.StatisticsIndex.staff.csvFileTitle": { + "defaultMessage": "员工统计" + }, "course.statistics.StatisticsIndex.staff.error": { "defaultMessage": "获取员工统计信息时出了点问题!请刷新重试。" }, @@ -6266,21 +6617,24 @@ "course.statistics.StatisticsIndex.staff.numStudents": { "defaultMessage": "# 学生" }, + "course.statistics.StatisticsIndex.staff.searchBar": { + "defaultMessage": "按员工姓名搜索" + }, "course.statistics.StatisticsIndex.staff.stddev": { "defaultMessage": "标准偏差" }, "course.statistics.StatisticsIndex.staff.tableTitle": { "defaultMessage": "员工统计" }, - "course.statistics.StatisticsIndex.staff.csvFileTitle": { - "defaultMessage": "员工统计" - }, - "course.statistics.StatisticsIndex.staff.searchBar": { - "defaultMessage": "按员工姓名搜索" - }, "course.statistics.StatisticsIndex.staffFailure": { "defaultMessage": "获取员工数据失败!" }, + "course.statistics.StatisticsIndex.students.csvFileTitle": { + "defaultMessage": "学生统计" + }, + "course.statistics.StatisticsIndex.students.email": { + "defaultMessage": "电子邮件" + }, "course.statistics.StatisticsIndex.students.error": { "defaultMessage": "获取学生数据时出了点问题!请刷新重试。" }, @@ -6296,12 +6650,12 @@ "course.statistics.StatisticsIndex.students.name": { "defaultMessage": "姓名" }, - "course.statistics.StatisticsIndex.students.email": { - "defaultMessage": "电子邮件" - }, "course.statistics.StatisticsIndex.students.noStudents": { "defaultMessage": "该课程还没有学生" }, + "course.statistics.StatisticsIndex.students.searchBar": { + "defaultMessage": "按学生姓名、学生类型或外部编号搜索" + }, "course.statistics.StatisticsIndex.students.showMyStudentsOnly": { "defaultMessage": "只显示我的学生" }, @@ -6320,12 +6674,6 @@ "course.statistics.StatisticsIndex.students.videoSubmissionCount": { "defaultMessage": "已观看的视频(总计:{courseVideoCount})" }, - "course.statistics.StatisticsIndex.students.csvFileTitle": { - "defaultMessage": "学生统计" - }, - "course.statistics.StatisticsIndex.students.searchBar": { - "defaultMessage": "按学生姓名、学生类型或外部编号搜索" - }, "course.statistics.StatisticsIndex.studentsFailure": { "defaultMessage": "获取学生数据失败!" }, @@ -7016,6 +7364,27 @@ "course.userInvitation.InviteUsersRegistrationCode.registrationCodeNote": { "defaultMessage": "已被邀请并使用此邀请代码注册课程的用户,不会在邀请页面中显示正确状态。" }, + "course.userInvitations.ExternalIdConflictPrompt.body": { + "defaultMessage": "这些用户已经注册或有待处理的邀请。不会向他们发送新的邀请邮件。您希望保留其当前的外部编号,还是用文件中的值替换它们?" + }, + "course.userInvitations.ExternalIdConflictPrompt.goBack": { + "defaultMessage": "返回" + }, + "course.userInvitations.ExternalIdConflictPrompt.keepExisting": { + "defaultMessage": "保留现有" + }, + "course.userInvitations.ExternalIdConflictPrompt.pendingCourseUserUpdates": { + "defaultMessage": "待处理课程成员更新({count})" + }, + "course.userInvitations.ExternalIdConflictPrompt.pendingInvitationUpdates": { + "defaultMessage": "待处理邀请更新({count})" + }, + "course.userInvitations.ExternalIdConflictPrompt.replace": { + "defaultMessage": "替换" + }, + "course.userInvitations.ExternalIdConflictPrompt.title": { + "defaultMessage": "确认外部编号更新" + }, "course.userInvitations.IndividualInvitations.appendNewRow": { "defaultMessage": "添加行" }, @@ -7031,6 +7400,33 @@ "course.userInvitations.IndividualInvitations.removeInvitation": { "defaultMessage": "删除邀请" }, + "course.userInvitations.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "你确定要删除对{name} ({email}) 的邀请吗?" + }, + "course.userInvitations.InvitationActionButtons.deletionFailure": { + "defaultMessage": "无法删除用户 - {error}" + }, + "course.userInvitations.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "对 {name} 的邀请已删除。" + }, + "course.userInvitations.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "删除邀请" + }, + "course.userInvitations.InvitationActionButtons.resendFailure": { + "defaultMessage": "无法重新发送邀请。" + }, + "course.userInvitations.InvitationActionButtons.resendSuccess": { + "defaultMessage": "向 {email} 重新发送电子邮件邀请!" + }, + "course.userInvitations.InvitationActionButtons.resendTooltip": { + "defaultMessage": "重新发送邀请" + }, + "course.userInvitations.InvitationResultDialog.actionableTitle": { + "defaultMessage": "失败({count})" + }, + "course.userInvitations.InvitationResultDialog.blankHeaderWarning": { + "defaultMessage": "有一个或多个列缺少标题,其数据已被忽略。" + }, "course.userInvitations.InvitationResultDialog.body": { "defaultMessage": "{newInvitationsCount, plural, =0 {No new users were} one {# new user has been} other {# new users have been}} 已被邀请到 Coursemology。 {newCourseUsersCount, plural, =0 {No user with Coursemology account has been} one {# new user with existing Coursemology account has been} other {# new users with existing Coursemology accounts have been}} 添加到此课程。" }, @@ -7049,15 +7445,24 @@ "course.userInvitations.InvitationResultDialog.existingCourseUsersInfo": { "defaultMessage": "这些用户已在课程中注册。他们未被重新注册。" }, - "course.userInvitations.InvitationResultDialog.externalIdUpdatedInfo": { - "defaultMessage": "如有指定,外部编号已更新。" - }, "course.userInvitations.InvitationResultDialog.existingInvitations": { "defaultMessage": "现有邀请({count})" }, "course.userInvitations.InvitationResultDialog.existingInvitationsInfo": { "defaultMessage": "这些用户已有待处理的邀请。他们未被重新邀请。" }, + "course.userInvitations.InvitationResultDialog.externalIdUpdatedInfo": { + "defaultMessage": "如有指定,外部编号已更新。" + }, + "course.userInvitations.InvitationResultDialog.failedInvitations": { + "defaultMessage": "发送失败({count})" + }, + "course.userInvitations.InvitationResultDialog.failedInvitationsInfo": { + "defaultMessage": "发送邀请邮件时出错,请重试。" + }, + "course.userInvitations.InvitationResultDialog.failedRowsSubtitle": { + "defaultMessage": "红色高亮显示的 {count} 行发送失败" + }, "course.userInvitations.InvitationResultDialog.header": { "defaultMessage": "邀请摘要" }, @@ -7067,14 +7472,8 @@ "course.userInvitations.InvitationResultDialog.newInvitations": { "defaultMessage": "新邀请({count})" }, - "course.userInvitations.InvitationResultDialog.actionableTitle": { - "defaultMessage": "失败({count})" - }, - "course.userInvitations.InvitationResultDialog.failedInvitations": { - "defaultMessage": "发送失败({count})" - }, - "course.userInvitations.InvitationResultDialog.failedInvitationsInfo": { - "defaultMessage": "发送邀请邮件时出错,请重试。" + "course.userInvitations.InvitationResultDialog.summary": { + "defaultMessage": "已发出 {newInvitations} 封新邀请,{newEnrollments} 人直接加入,{alreadyInCourse} 人已在课程中。" }, "course.userInvitations.InvitationResultDialog.summaryFailed": { "defaultMessage": "{count} 个失败。" @@ -7082,14 +7481,8 @@ "course.userInvitations.InvitationResultDialog.updatedSubtitle": { "defaultMessage": "{count} 条已更新 · 优先显示" }, - "course.userInvitations.InvitationResultDialog.blankHeaderWarning": { - "defaultMessage": "有一个或多个列缺少标题,其数据已被忽略。" - }, - "course.userInvitations.InvitationResultDialog.summary": { - "defaultMessage": "已发出 {newInvitations} 封新邀请,{newEnrollments} 人直接加入,{alreadyInCourse} 人已在课程中。" - }, - "course.userInvitations.InvitationResultDialog.failedRowsSubtitle": { - "defaultMessage": "红色高亮显示的 {count} 行发送失败" + "course.userInvitations.InvitationResultExistingTable.previouslyLabel": { + "defaultMessage": "之前的值:{value}" }, "course.userInvitations.InvitationResultFailedTable.duplicateEmailInFile": { "defaultMessage": "上传文件中存在重复的电子邮件地址" @@ -7100,18 +7493,15 @@ "course.userInvitations.InvitationResultFailedTable.externalIdTaken": { "defaultMessage": "该外部编号已分配给另一名课程成员" }, + "course.userInvitations.InvitationResultFailedTable.externalIdTakenEnrolled": { + "defaultMessage": "已是课程成员 - 外部编号未能应用(已分配给另一名成员)" + }, "course.userInvitations.InvitationResultFailedTable.failedToSend": { "defaultMessage": "邀请邮件发送失败,请重试 - 如问题持续发生,请联系我们获取帮助" }, - "course.userInvitations.InvitationResultFailedTable.externalIdTakenEnrolled": { - "defaultMessage": "已是课程成员 — 外部编号未能应用(已分配给另一名成员)" - }, "course.userInvitations.InvitationResultSkippedTable.previouslyLabel": { "defaultMessage": "之前的值:{value}" }, - "course.userInvitations.InvitationResultExistingTable.previouslyLabel": { - "defaultMessage": "之前的值:{value}" - }, "course.userInvitations.InvitationsBarChart.accepted": { "defaultMessage": "已接受邀请" }, @@ -7148,14 +7538,17 @@ "course.userInvitations.InviteUsersFileUpload.failureGeneric": { "defaultMessage": "邀请用户失败。请确保你的数据格式正确。" }, + "course.userInvitations.InviteUsersFileUpload.fileRequired": { + "defaultMessage": "请选择要上传的 CSV 文件。" + }, "course.userInvitations.InviteUsersFileUpload.fileUploadExample": { "defaultMessage": "姓名,电子邮件,外部编号,角色,旁听学生{br}John,test1@example.com,A0123456,student,y{br}Mary,test2@example.com,A0123457,teaching_assistant,n" }, "course.userInvitations.InviteUsersFileUpload.fileUploadExamplePersonalTimeline": { "defaultMessage": "姓名,电子邮件,外部编号,角色,旁听学生,个人时间线{br}John,test1@example.com,A0123456,student,y,otot{br}Mary,test2@example.com,A0123457,teaching_assistant,n,fixed" }, - "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRequired": { - "defaultMessage": "CSV 必须同时包含“姓名”和“电子邮件”列。所有其他列均为可选。" + "course.userInvitations.InviteUsersFileUpload.fileUploadInfo": { + "defaultMessage": "上传具有以下格式的 .csv 文件:" }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoEmail": { "defaultMessage": "每个邀请在课程内必须使用唯一的电子邮件地址。重复的电子邮件将被跳过。" @@ -7163,24 +7556,21 @@ "course.userInvitations.InviteUsersFileUpload.fileUploadInfoExternalId": { "defaultMessage": "如果提供了外部编号,则这些外部编号在课程内必须唯一。" }, - "course.userInvitations.InviteUsersFileUpload.fileUploadInfo": { - "defaultMessage": "上传具有以下格式的 .csv 文件:" - }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoPersonalTimeline": { "defaultMessage": "个人时间线可以是[fixed, otot, stragglers, fomo],若省略则默认为 {defaultTimelineAlgorithm}。" }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoPhantom": { "defaultMessage": "旁听学生可以是 true/false,具有以下值 ['t', 'true', 'y', 'yes'](不区分大小写),若省略则默认为 false。" }, + "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRequired": { + "defaultMessage": "CSV 必须同时包含“姓名”和“电子邮件”列。所有其他列均为可选。" + }, "course.userInvitations.InviteUsersFileUpload.fileUploadInfoRole": { "defaultMessage": "角色可以是[student, observer, teaching_assistant, manager, owner],如果省略则默认为学生。用户只能被助教邀请为学生。" }, "course.userInvitations.InviteUsersFileUpload.importInProgress": { "defaultMessage": "正在导入用户,请稍候…" }, - "course.userInvitations.InviteUsersFileUpload.fileRequired": { - "defaultMessage": "请选择要上传的 CSV 文件。" - }, "course.userInvitations.InviteUsersFileUpload.template": { "defaultMessage": "(模板文件)" }, @@ -7193,27 +7583,6 @@ "course.userInvitations.InviteUsersfileUploadForm.invite": { "defaultMessage": "从文件邀请用户" }, - "course.userInvitations.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "你确定要删除对{name} ({email}) 的邀请吗?" - }, - "course.userInvitations.InvitationActionButtons.deletionFailure": { - "defaultMessage": "无法删除用户 - {error}" - }, - "course.userInvitations.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "对 {name} 的邀请已删除。" - }, - "course.userInvitations.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "删除邀请" - }, - "course.userInvitations.InvitationActionButtons.resendFailure": { - "defaultMessage": "无法重新发送邀请。" - }, - "course.userInvitations.InvitationActionButtons.resendSuccess": { - "defaultMessage": "向 {email} 重新发送电子邮件邀请!" - }, - "course.userInvitations.InvitationActionButtons.resendTooltip": { - "defaultMessage": "重新发送邀请" - }, "course.userInvitations.RegistrationCodeButton.registrationCode": { "defaultMessage": "注册码" }, @@ -7232,24 +7601,24 @@ "course.userInvitations.UserInvitationsTable.accepted": { "defaultMessage": "已接受" }, + "course.userInvitations.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "已于{confirmedAt}接受" + }, "course.userInvitations.UserInvitationsTable.failed": { "defaultMessage": "失败" }, "course.userInvitations.UserInvitationsTable.noInvitations": { "defaultMessage": "没有邀请。" }, - "course.userInvitations.UserInvitationsTable.searchText": { - "defaultMessage": "按姓名、电子邮件或外部ID搜索" - }, "course.userInvitations.UserInvitationsTable.pending": { "defaultMessage": "待处理" }, + "course.userInvitations.UserInvitationsTable.searchText": { + "defaultMessage": "按姓名、电子邮件或外部ID搜索" + }, "course.userInvitations.UserInvitationsTable.sentTooltip": { "defaultMessage": "已于{sentAt}发送" }, - "course.userInvitations.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "已于{confirmedAt}接受" - }, "course.userNotification.AchievementGainedPopup.unlocked": { "defaultMessage": "成就解锁!" }, @@ -7286,6 +7655,12 @@ "course.users.ManageUsersTable.ManageUsersTable.searchText": { "defaultMessage": "按姓名、电子邮件或外部ID搜索" }, + "course.users.ManageUsersTable.addIdFailure": { + "defaultMessage": "无法将外部编号设置为 {newId}" + }, + "course.users.ManageUsersTable.addIdSuccess": { + "defaultMessage": "外部编号已设置为 {newId}" + }, "course.users.ManageUsersTable.assignToTimeline": { "defaultMessage": "分配到时间线" }, @@ -7316,6 +7691,12 @@ "course.users.ManageUsersTable.changeAlgorithmSuccess": { "defaultMessage": "将 {name} 的时间线算法更新到 {timeline}" }, + "course.users.ManageUsersTable.changeIdFailure": { + "defaultMessage": "无法将编号从 {oldId} 更改为 {newId}" + }, + "course.users.ManageUsersTable.changeIdSuccess": { + "defaultMessage": "编号已从 {oldId} 更改为 {newId}" + }, "course.users.ManageUsersTable.changeRoleFailure": { "defaultMessage": "无法将 {name} 的角色更改为 {role}。" }, @@ -7331,29 +7712,29 @@ "course.users.ManageUsersTable.defaultTimeline": { "defaultMessage": "默认" }, - "course.users.ManageUsersTable.group": { - "defaultMessage": "组:{name}" + "course.users.ManageUsersTable.deleteIdFailure": { + "defaultMessage": "无法删除外部编号" }, - "course.users.ManageUsersTable.phantomSuccess": { - "defaultMessage": "{name} {isPhantom, select, true {现在是旁听用户} other {现在是普通用户} }。" + "course.users.ManageUsersTable.deleteIdSuccess": { + "defaultMessage": "外部编号已删除" }, - "course.users.ManageUsersTable.addIdFailure": { - "defaultMessage": "无法将外部编号设置为 {newId}" + "course.users.ManageUsersTable.deletionConfirm": { + "defaultMessage": "你确定要删除{role} {name} ({email}) 吗?" }, - "course.users.ManageUsersTable.addIdSuccess": { - "defaultMessage": "外部编号已设置为 {newId}" + "course.users.ManageUsersTable.deletionFailure": { + "defaultMessage": "删除用户失败。" }, - "course.users.ManageUsersTable.changeIdFailure": { - "defaultMessage": "无法将编号从 {oldId} 更改为 {newId}" + "course.users.ManageUsersTable.deletionScheduled": { + "defaultMessage": "{role} {name} ({email}) 已计划删除。" }, - "course.users.ManageUsersTable.changeIdSuccess": { - "defaultMessage": "编号已从 {oldId} 更改为 {newId}" + "course.users.ManageUsersTable.deletionSuccess": { + "defaultMessage": "用户已被删除。" }, - "course.users.ManageUsersTable.deleteIdFailure": { - "defaultMessage": "无法删除外部编号" + "course.users.ManageUsersTable.group": { + "defaultMessage": "组:{name}" }, - "course.users.ManageUsersTable.deleteIdSuccess": { - "defaultMessage": "外部编号已删除" + "course.users.ManageUsersTable.phantomSuccess": { + "defaultMessage": "{name} {isPhantom, select, true {现在是旁听用户} other {现在是普通用户} }。" }, "course.users.ManageUsersTable.renameFailure": { "defaultMessage": "无法将 {oldName} 重命名为 {newName}" @@ -7364,6 +7745,24 @@ "course.users.ManageUsersTable.selectedNUsers": { "defaultMessage": "已选择 {n} 个用户" }, + "course.users.ManageUsersTable.suspend": { + "defaultMessage": "暂停" + }, + "course.users.ManageUsersTable.suspendFailure": { + "defaultMessage": "无法暂停 {name}。" + }, + "course.users.ManageUsersTable.suspendSuccess": { + "defaultMessage": "{name} 现已被暂停。在取消暂停之前,他们无法访问此课程。" + }, + "course.users.ManageUsersTable.unsuspend": { + "defaultMessage": "取消暂停" + }, + "course.users.ManageUsersTable.unsuspendFailure": { + "defaultMessage": "无法取消暂停 {name}。" + }, + "course.users.ManageUsersTable.unsuspendSuccess": { + "defaultMessage": "{name} 不再被暂停。他们现在可以访问课程。" + }, "course.users.ManageUsersTable.updateFailure": { "defaultMessage": "无法更新用户 - {error}" }, @@ -7472,36 +7871,6 @@ "course.users.UpgradeToStaff.upgradeSuccess": { "defaultMessage": "{count, plural, =0 {无用户} one {# 新用户已经} other {# 新用户已经}} 升级到 {role}" }, - "course.users.ManageUsersTable.deletionConfirm": { - "defaultMessage": "你确定要删除{role} {name} ({email}) 吗?" - }, - "course.users.ManageUsersTable.deletionFailure": { - "defaultMessage": "删除用户失败。" - }, - "course.users.ManageUsersTable.deletionScheduled": { - "defaultMessage": "{role} {name} ({email}) 已计划删除。" - }, - "course.users.ManageUsersTable.deletionSuccess": { - "defaultMessage": "用户已被删除。" - }, - "course.users.ManageUsersTable.suspend": { - "defaultMessage": "暂停" - }, - "course.users.ManageUsersTable.suspendFailure": { - "defaultMessage": "无法暂停 {name}。" - }, - "course.users.ManageUsersTable.suspendSuccess": { - "defaultMessage": "{name} 现已被暂停。在取消暂停之前,他们无法访问此课程。" - }, - "course.users.ManageUsersTable.unsuspend": { - "defaultMessage": "取消暂停" - }, - "course.users.ManageUsersTable.unsuspendFailure": { - "defaultMessage": "无法取消暂停 {name}。" - }, - "course.users.ManageUsersTable.unsuspendSuccess": { - "defaultMessage": "{name} 不再被暂停。他们现在可以访问课程。" - }, "course.users.UserManagementTabs.enrolRequestsTitle": { "defaultMessage": "注册请求" }, @@ -7640,27 +8009,27 @@ "course.video.VideoShow.videoTitle": { "defaultMessage": "视频 - {title}" }, + "course.video.VideoTable.actions": { + "defaultMessage": "操作" + }, + "course.video.VideoTable.averageWatched": { + "defaultMessage": "平均观看百分比" + }, "course.video.VideoTable.noVideo": { "defaultMessage": "没有视频" }, - "course.video.VideoTable.title": { - "defaultMessage": "标题" + "course.video.VideoTable.published": { + "defaultMessage": "已发布" }, "course.video.VideoTable.startAt": { "defaultMessage": "开始于" }, + "course.video.VideoTable.title": { + "defaultMessage": "标题" + }, "course.video.VideoTable.watchCount": { "defaultMessage": "观看次数" }, - "course.video.VideoTable.averageWatched": { - "defaultMessage": "平均观看百分比" - }, - "course.video.VideoTable.published": { - "defaultMessage": "已发布" - }, - "course.video.VideoTable.actions": { - "defaultMessage": "操作" - }, "course.video.VideosIndex.fetchVideosFailure": { "defaultMessage": "无法检索视频。" }, @@ -7847,12 +8216,12 @@ "lib.components.core.Expandable.showMore": { "defaultMessage": "显示更多" }, - "lib.components.core.Note.noteHeader": { - "defaultMessage": "提示" - }, "lib.components.core.Note.errorHeader": { "defaultMessage": "错误" }, + "lib.components.core.Note.noteHeader": { + "defaultMessage": "提示" + }, "lib.components.core.banners.ServerUnreachableBanner.refreshPage": { "defaultMessage": "刷新页面" }, @@ -7943,44 +8312,110 @@ "lib.components.extensions.conditions.empty": { "defaultMessage": "未添加条件" }, - "lib.components.extensions.conditions.errorOccurredWhenCreatingCondition": { - "defaultMessage": "创建此条件时出错。" + "lib.components.extensions.conditions.errorOccurredWhenCreatingCondition": { + "defaultMessage": "创建此条件时出错。" + }, + "lib.components.extensions.conditions.errorOccurredWhenDeletingCondition": { + "defaultMessage": "删除此条件时出错。" + }, + "lib.components.extensions.conditions.errorOccurredWhenUpdatingCondition": { + "defaultMessage": "更新此条件时出错。" + }, + "lib.components.extensions.conditions.level": { + "defaultMessage": "等级" + }, + "lib.components.extensions.conditions.scoreZeroPercentNotice": { + "defaultMessage": "请注意,“得分至少为 0%”要求在满足此条件之前对,该测验进行评分。如果未指定最低等级,则此条件仅要求提交。" + }, + "lib.components.extensions.conditions.scoringAtLeast": { + "defaultMessage": "至少得分" + }, + "lib.components.extensions.conditions.specifyLevel": { + "defaultMessage": "指定最低等级" + }, + "lib.components.extensions.conditions.survey": { + "defaultMessage": "民意调查" + }, + "lib.components.extensions.conditions.type": { + "defaultMessage": "类型" + }, + "lib.components.extensions.conditions.updateCondition": { + "defaultMessage": "更新条件" + }, + "lib.components.form.fields.DateTimePickerField.invalidDateTime": { + "defaultMessage": "无效的日期和/或时间" + }, + "lib.components.form.fields.SingleFileInput.dropzone": { + "defaultMessage": "将文件拖到此处或上传文件" + }, + "lib.components.form.fields.SingleFileInput.removeFile": { + "defaultMessage": "删除文件" + }, + "lib.components.getHelp.filter.filterAssessmentLabel": { + "defaultMessage": "按测验筛选" + }, + "lib.components.getHelp.filter.filterCourseLabel": { + "defaultMessage": "按课程筛选" + }, + "lib.components.getHelp.filter.filterEndDateLabel": { + "defaultMessage": "结束日期" + }, + "lib.components.getHelp.filter.filterStartDateLabel": { + "defaultMessage": "开始日期" + }, + "lib.components.getHelp.filter.filterStudentLabel": { + "defaultMessage": "按学生筛选" + }, + "lib.components.getHelp.filter.lastFourteenDays": { + "defaultMessage": "最近14天" + }, + "lib.components.getHelp.filter.lastSevenDays": { + "defaultMessage": "最近7天" + }, + "lib.components.getHelp.filter.lastSixMonths": { + "defaultMessage": "最近6个月" + }, + "lib.components.getHelp.filter.lastThirtyDays": { + "defaultMessage": "最近30天" + }, + "lib.components.getHelp.filter.lastTwelveMonths": { + "defaultMessage": "最近12个月" }, - "lib.components.extensions.conditions.errorOccurredWhenDeletingCondition": { - "defaultMessage": "删除此条件时出错。" + "lib.components.getHelp.header": { + "defaultMessage": "最近的获取帮助活动 ({total, plural, other {#个对话}})" }, - "lib.components.extensions.conditions.errorOccurredWhenUpdatingCondition": { - "defaultMessage": "更新此条件时出错。" + "lib.components.getHelp.table.assessmentTitle": { + "defaultMessage": "测验" }, - "lib.components.extensions.conditions.level": { - "defaultMessage": "等级" + "lib.components.getHelp.table.courseTitle": { + "defaultMessage": "课程" }, - "lib.components.extensions.conditions.scoreZeroPercentNotice": { - "defaultMessage": "请注意,“得分至少为 0%”要求在满足此条件之前对,该测验进行评分。如果未指定最低等级,则此条件仅要求提交。" + "lib.components.getHelp.table.createdAt": { + "defaultMessage": "最后消息时间" }, - "lib.components.extensions.conditions.scoringAtLeast": { - "defaultMessage": "至少得分" + "lib.components.getHelp.table.instanceTitle": { + "defaultMessage": "实例" }, - "lib.components.extensions.conditions.specifyLevel": { - "defaultMessage": "指定最低等级" + "lib.components.getHelp.table.lastMessage": { + "defaultMessage": "最后消息" }, - "lib.components.extensions.conditions.survey": { - "defaultMessage": "民意调查" + "lib.components.getHelp.table.messageCount": { + "defaultMessage": "消息数" }, - "lib.components.extensions.conditions.type": { - "defaultMessage": "类型" + "lib.components.getHelp.table.questionNumber": { + "defaultMessage": "问题" }, - "lib.components.extensions.conditions.updateCondition": { - "defaultMessage": "更新条件" + "lib.components.getHelp.table.studentName": { + "defaultMessage": "姓名" }, - "lib.components.form.fields.DateTimePickerField.invalidDateTime": { - "defaultMessage": "无效的日期和/或时间" + "lib.components.getHelp.validation.endDateBeforeStartDate": { + "defaultMessage": "结束日期必须大于或等于开始日期" }, - "lib.components.form.fields.SingleFileInput.dropzone": { - "defaultMessage": "将文件拖到此处或上传文件" + "lib.components.getHelp.validation.exceedDateRange": { + "defaultMessage": "日期范围不能超过365天" }, - "lib.components.form.fields.SingleFileInput.removeFile": { - "defaultMessage": "删除文件" + "lib.components.getHelp.validation.invalidDateSelection": { + "defaultMessage": "无效日期" }, "lib.components.navigation.AdminPopupMenuList.adminPanel": { "defaultMessage": "系统管理面板" @@ -8018,6 +8453,21 @@ "lib.components.navigation.CourseSwitcherPopupMenu.thisCourse": { "defaultMessage": "此课程" }, + "lib.components.table.MuiColumnPickerPrompt.apply": { + "defaultMessage": "应用" + }, + "lib.components.table.MuiColumnPickerPrompt.cancel": { + "defaultMessage": "取消" + }, + "lib.components.table.MuiColumnPickerPrompt.defaultTitle": { + "defaultMessage": "选择列" + }, + "lib.components.table.MuiTableToolbar.directExport": { + "defaultMessage": "导出" + }, + "lib.components.table.MuiTableToolbar.exportTrigger": { + "defaultMessage": "导出…" + }, "lib.hooks.router.usePrompt.sureYouWantToLeave": { "defaultMessage": "确定要离开此页面吗?你将丢失未保存的更改。" }, @@ -8045,108 +8495,27 @@ "lib.translations.beta": { "defaultMessage": "Beta" }, - "lib.components.getHelp.header": { - "defaultMessage": "最近的获取帮助活动 ({total, plural, other {#个对话}})" - }, - "lib.components.getHelp.filter.filterCourseLabel": { - "defaultMessage": "按课程筛选" - }, - "lib.components.getHelp.filter.filterAssessmentLabel": { - "defaultMessage": "按测验筛选" - }, - "lib.components.getHelp.filter.filterStudentLabel": { - "defaultMessage": "按学生筛选" - }, - "lib.components.getHelp.filter.filterStartDateLabel": { - "defaultMessage": "开始日期" - }, - "lib.components.getHelp.filter.filterEndDateLabel": { - "defaultMessage": "结束日期" - }, - "lib.components.getHelp.filter.lastSevenDays": { - "defaultMessage": "最近7天" - }, - "lib.components.getHelp.filter.lastFourteenDays": { - "defaultMessage": "最近14天" - }, - "lib.components.getHelp.filter.lastThirtyDays": { - "defaultMessage": "最近30天" - }, - "lib.components.getHelp.filter.lastSixMonths": { - "defaultMessage": "最近6个月" - }, - "lib.components.getHelp.filter.lastTwelveMonths": { - "defaultMessage": "最近12个月" - }, - "lib.components.getHelp.table.studentName": { - "defaultMessage": "姓名" - }, - "lib.components.getHelp.table.messageCount": { - "defaultMessage": "消息数" - }, - "lib.components.getHelp.table.lastMessage": { - "defaultMessage": "最后消息" - }, - "lib.components.getHelp.table.questionNumber": { - "defaultMessage": "问题" - }, - "lib.components.getHelp.table.assessmentTitle": { - "defaultMessage": "测验" - }, - "lib.components.getHelp.table.createdAt": { - "defaultMessage": "最后消息时间" - }, - "lib.components.getHelp.table.courseTitle": { - "defaultMessage": "课程" - }, - "lib.components.getHelp.table.instanceTitle": { - "defaultMessage": "实例" - }, - "lib.components.getHelp.validation.invalidDateSelection": { - "defaultMessage": "无效日期" - }, - "lib.components.getHelp.validation.endDateBeforeStartDate": { - "defaultMessage": "结束日期必须大于或等于开始日期" - }, - "lib.components.getHelp.validation.exceedDateRange": { - "defaultMessage": "日期范围不能超过365天" - }, - "lib.components.table.MuiColumnPickerPrompt.apply": { - "defaultMessage": "应用" - }, - "lib.components.table.MuiColumnPickerPrompt.cancel": { - "defaultMessage": "取消" - }, - "lib.components.table.MuiColumnPickerPrompt.defaultTitle": { - "defaultMessage": "选择列" - }, - "lib.components.table.MuiTableToolbar.directExport": { - "defaultMessage": "导出" - }, - "lib.components.table.MuiTableToolbar.exportTrigger": { - "defaultMessage": "导出…" - }, "lib.translations.course.users.fetchUsersFailure": { "defaultMessage": "无法获取用户。" }, "lib.translations.course.users.manageUsersHeader": { "defaultMessage": "管理用户" }, - "lib.translations.course.users.roles.student": { - "defaultMessage": "学生" - }, - "lib.translations.course.users.roles.teachingAssistant": { - "defaultMessage": "助教" + "lib.translations.course.users.roles.manager": { + "defaultMessage": "管理员" }, "lib.translations.course.users.roles.observer": { "defaultMessage": "观察者" }, - "lib.translations.course.users.roles.manager": { - "defaultMessage": "管理员" - }, "lib.translations.course.users.roles.owner": { "defaultMessage": "拥有者" }, + "lib.translations.course.users.roles.student": { + "defaultMessage": "学生" + }, + "lib.translations.course.users.roles.teachingAssistant": { + "defaultMessage": "助教" + }, "lib.translations.experimental": { "defaultMessage": "实验性的" }, @@ -8258,14 +8627,14 @@ "lib.translations.form.validation.startEndDateValidationError": { "defaultMessage": "必须在开始日期之后" }, - "lib.translations.instance.users.roles.normal": { - "defaultMessage": "普通用户" + "lib.translations.instance.users.roles.administrator": { + "defaultMessage": "平台管理员" }, "lib.translations.instance.users.roles.instructor": { "defaultMessage": "教学导师" }, - "lib.translations.instance.users.roles.administrator": { - "defaultMessage": "平台管理员" + "lib.translations.instance.users.roles.normal": { + "defaultMessage": "普通用户" }, "lib.translations.messages.fetchingError": { "defaultMessage": "加载数据时出错。请重新加载并重试。" @@ -8276,17 +8645,26 @@ "lib.translations.messages.loadImageError": { "defaultMessage": "加载图片时出错。请尝试选择另一张。" }, + "lib.translations.myStudents": { + "defaultMessage": "我的学生" + }, "lib.translations.myStudentsIncludingPhantoms": { "defaultMessage": "我的学生(包括旁听学生)" }, - "lib.translations.studentsIncludingPhantoms": { - "defaultMessage": "学生(包括旁听学生)" + "lib.translations.no": { + "defaultMessage": "否" + }, + "lib.translations.staff": { + "defaultMessage": "职员" }, "lib.translations.staffIncludingPhantoms": { "defaultMessage": "职员(包括旁听学生)" }, - "lib.translations.no": { - "defaultMessage": "否" + "lib.translations.students": { + "defaultMessage": "学生" + }, + "lib.translations.studentsIncludingPhantoms": { + "defaultMessage": "学生(包括旁听学生)" }, "lib.translations.summary": { "defaultMessage": "概括" @@ -8324,15 +8702,15 @@ "lib.translations.table.column.createdAt": { "defaultMessage": "创建于" }, + "lib.translations.table.column.currentExternalId": { + "defaultMessage": "当前外部编号" + }, "lib.translations.table.column.designation": { "defaultMessage": "职位" }, "lib.translations.table.column.email": { "defaultMessage": "电子邮件" }, - "lib.translations.table.column.externalId": { - "defaultMessage": "外部编号" - }, "lib.translations.table.column.endAt": { "defaultMessage": "结束于" }, @@ -8342,6 +8720,9 @@ "lib.translations.table.column.experiencePointsAwarded": { "defaultMessage": "获得的经验值" }, + "lib.translations.table.column.externalId": { + "defaultMessage": "外部编号" + }, "lib.translations.table.column.groups": { "defaultMessage": "组" }, @@ -8384,6 +8765,9 @@ "lib.translations.table.column.name": { "defaultMessage": "姓名" }, + "lib.translations.table.column.newExternalId": { + "defaultMessage": "新外部编号" + }, "lib.translations.table.column.optional": { "defaultMessage": "选填" }, @@ -8465,30 +8849,15 @@ "material.attemptLoader.errorAccessingMaterial": { "defaultMessage": "获取该资料时发生错误,请稍后再试。" }, - "system.admin.instance.instance.InstanceAdminNavigator.announcements": { - "defaultMessage": "公告" - }, - "system.admin.instance.instance.InstanceAdminNavigator.components": { - "defaultMessage": "组件" - }, - "system.admin.instance.instance.InstanceAdminNavigator.courses": { - "defaultMessage": "课程" - }, - "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { - "defaultMessage": "角色要求" - }, - "system.admin.instance.instance.InstanceAdminNavigator.users": { - "defaultMessage": "用户" - }, - "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { - "defaultMessage": "获取帮助" - }, "system.admin.admin.AdminNavigator.announcements": { "defaultMessage": "系统公告" }, "system.admin.admin.AdminNavigator.courses": { "defaultMessage": "课程" }, + "system.admin.admin.AdminNavigator.getHelp": { + "defaultMessage": "获取帮助" + }, "system.admin.admin.AdminNavigator.instances": { "defaultMessage": "实例" }, @@ -8498,9 +8867,6 @@ "system.admin.admin.AdminNavigator.users": { "defaultMessage": "用户" }, - "system.admin.admin.AdminNavigator.getHelp": { - "defaultMessage": "获取帮助" - }, "system.admin.admin.AnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "无法获取公告" }, @@ -8591,18 +8957,18 @@ "system.admin.admin.UsersButton.deletionConfirm": { "defaultMessage": "您确定要继续此操作吗?" }, - "system.admin.admin.UsersButton.deletionFailure": { - "defaultMessage": "删除用户失败 - {error}" - }, - "system.admin.admin.UsersButton.deletionSuccess": { - "defaultMessage": "用户已被删除。" - }, "system.admin.admin.UsersButton.deletionConfirmTitle": { "defaultMessage": "正在删除{role}用户 {name}({email})" }, + "system.admin.admin.UsersButton.deletionFailure": { + "defaultMessage": "删除用户失败 - {error}" + }, "system.admin.admin.UsersButton.deletionPromptContent": { "defaultMessage": "删除该用户将永久删除以下{count, plural, one {课程} other {课程}}中的相关数据:" }, + "system.admin.admin.UsersButton.deletionSuccess": { + "defaultMessage": "用户已被删除。" + }, "system.admin.admin.UsersIndex.activeUsers": { "defaultMessage": "活跃用户:{allCount}({adminCount} 管理员,{normalCount} 普通用户){br}(过去7天内活跃)" }, @@ -8618,9 +8984,6 @@ "system.admin.admin.UsersTable.changeRoleSuccess": { "defaultMessage": "已成功将 {name} 的角色更改为 {role}。" }, - "system.admin.users.UsersTable.instanceEntry": { - "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one {(1 门课程)} other {({courseCount} 门课程)}}" - }, "system.admin.admin.UsersTable.renameSuccess": { "defaultMessage": "{oldName} 已重命名为 {newName}。" }, @@ -8642,11 +9005,29 @@ "system.admin.instance.instance.IndividualInvitation.removeInvitation": { "defaultMessage": "删除邀请" }, - "system.admin.instance.instance.IndividualInvitations.appendNewRow": { - "defaultMessage": "添加行" + "system.admin.instance.instance.IndividualInvitations.appendNewRow": { + "defaultMessage": "添加行" + }, + "system.admin.instance.instance.IndividualInvitations.invite": { + "defaultMessage": "邀请所有用户" + }, + "system.admin.instance.instance.InstanceAdminNavigator.announcements": { + "defaultMessage": "公告" + }, + "system.admin.instance.instance.InstanceAdminNavigator.components": { + "defaultMessage": "组件" + }, + "system.admin.instance.instance.InstanceAdminNavigator.courses": { + "defaultMessage": "课程" + }, + "system.admin.instance.instance.InstanceAdminNavigator.getHelp": { + "defaultMessage": "获取帮助" + }, + "system.admin.instance.instance.InstanceAdminNavigator.roleRequests": { + "defaultMessage": "角色要求" }, - "system.admin.instance.instance.IndividualInvitations.invite": { - "defaultMessage": "邀请所有用户" + "system.admin.instance.instance.InstanceAdminNavigator.users": { + "defaultMessage": "用户" }, "system.admin.instance.instance.InstanceAnnouncementsIndex.fetchAnnouncementsFailure": { "defaultMessage": "无法获取公告" @@ -8768,6 +9149,27 @@ "system.admin.instance.instance.InstanceUsersTabs.usersTab": { "defaultMessage": "用户" }, + "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { + "defaultMessage": "你确定要删除对{name} ({email}) 的邀请吗?" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { + "defaultMessage": "无法删除用户 - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { + "defaultMessage": "{name} 的邀请已删除。" + }, + "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { + "defaultMessage": "删除邀请" + }, + "system.admin.instance.instance.InvitationActionButtons.resendFailure": { + "defaultMessage": "未能重新发送邀请 - {error}" + }, + "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { + "defaultMessage": "向 {email} 重新发送电子邮件邀请!" + }, + "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { + "defaultMessage": "重新发送邀请" + }, "system.admin.instance.instance.InvitationResultDialog.close": { "defaultMessage": "关闭" }, @@ -8798,27 +9200,6 @@ "system.admin.instance.instance.InvitationResultDialog.newInvitations": { "defaultMessage": "新邀请({count})" }, - "system.admin.instance.instance.InvitationActionButtons.deletionConfirm": { - "defaultMessage": "你确定要删除对{name} ({email}) 的邀请吗?" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionFailure": { - "defaultMessage": "无法删除用户 - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionSuccess": { - "defaultMessage": "{name} 的邀请已删除。" - }, - "system.admin.instance.instance.InvitationActionButtons.deletionTooltip": { - "defaultMessage": "删除邀请" - }, - "system.admin.instance.instance.InvitationActionButtons.resendFailure": { - "defaultMessage": "未能重新发送邀请 - {error}" - }, - "system.admin.instance.instance.InvitationActionButtons.resendSuccess": { - "defaultMessage": "向 {email} 重新发送电子邮件邀请!" - }, - "system.admin.instance.instance.InvitationActionButtons.resendTooltip": { - "defaultMessage": "重新发送邀请" - }, "system.admin.instance.instance.PendingRoleRequestsButton.approveFailure": { "defaultMessage": "未能批准角色请求 - {error}" }, @@ -8864,6 +9245,9 @@ "system.admin.instance.instance.UserInvitationsTable.accepted": { "defaultMessage": "已接受" }, + "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { + "defaultMessage": "已于{confirmedAt}接受" + }, "system.admin.instance.instance.UserInvitationsTable.failed": { "defaultMessage": "失败" }, @@ -8876,27 +9260,24 @@ "system.admin.instance.instance.UserInvitationsTable.sentTooltip": { "defaultMessage": "已于{sentAt}发送" }, - "system.admin.instance.instance.UserInvitationsTable.confirmedTooltip": { - "defaultMessage": "已于{confirmedAt}接受" - }, "system.admin.instance.instance.UsersButton.deleteTooltip": { "defaultMessage": "移除用户" }, "system.admin.instance.instance.UsersButton.deletionConfirm": { "defaultMessage": "您确定要继续此操作吗?" }, - "system.admin.instance.instance.UsersButton.deletionFailure": { - "defaultMessage": "无法删除用户 - {error}" - }, - "system.admin.instance.instance.UsersButton.deletionSuccess": { - "defaultMessage": "用户已从该实例中移除。" - }, "system.admin.instance.instance.UsersButton.deletionConfirmTitle": { "defaultMessage": "正在移除{role}用户 {name}({email})" }, + "system.admin.instance.instance.UsersButton.deletionFailure": { + "defaultMessage": "无法删除用户 - {error}" + }, "system.admin.instance.instance.UsersButton.deletionPromptContent": { "defaultMessage": "移除该用户可能会导致以下{count, plural, one {课程} other {课程}}出现错误:" }, + "system.admin.instance.instance.UsersButton.deletionSuccess": { + "defaultMessage": "用户已从该实例中移除。" + }, "system.admin.instance.instance.UsersTable.changeRoleSuccess": { "defaultMessage": "已成功将 {name} 的角色更改为 {role}。" }, @@ -8918,6 +9299,9 @@ "system.admin.users.UsersTable.fetchFilteredUsersFailure": { "defaultMessage": "无法获取用户。" }, + "system.admin.users.UsersTable.instanceEntry": { + "defaultMessage": "{instanceName}{courseCount, plural, =0 {} one {(1 门课程)} other {({courseCount} 门课程)}}" + }, "user.accountSettings": { "defaultMessage": "账户设置" }, @@ -9220,191 +9604,5 @@ }, "users.troubleSigningIn": { "defaultMessage": "登录遇到问题?" - }, - "course.userInvitations.ExternalIdConflictPrompt.title": { - "defaultMessage": "确认外部编号更新" - }, - "course.userInvitations.ExternalIdConflictPrompt.body": { - "defaultMessage": "这些用户已经注册或有待处理的邀请。不会向他们发送新的邀请邮件。您希望保留其当前的外部编号,还是用文件中的值替换它们?" - }, - "course.userInvitations.ExternalIdConflictPrompt.pendingInvitationUpdates": { - "defaultMessage": "待处理邀请更新({count})" - }, - "course.userInvitations.ExternalIdConflictPrompt.pendingCourseUserUpdates": { - "defaultMessage": "待处理课程成员更新({count})" - }, - "course.userInvitations.ExternalIdConflictPrompt.goBack": { - "defaultMessage": "返回" - }, - "course.userInvitations.ExternalIdConflictPrompt.keepExisting": { - "defaultMessage": "保留现有" - }, - "course.userInvitations.ExternalIdConflictPrompt.replace": { - "defaultMessage": "替换" - }, - "lib.translations.table.column.currentExternalId": { - "defaultMessage": "当前外部编号" - }, - "lib.translations.table.column.newExternalId": { - "defaultMessage": "新外部编号" - }, - "course.admin.GradebookSettings.gradebookSettings": { - "defaultMessage": "成绩册设置" - }, - "course.admin.GradebookSettings.weightedViewEnabled": { - "defaultMessage": "启用加权成绩视图" - }, - "course.admin.GradebookSettings.weightedViewEnabledHint": { - "defaultMessage": "在成绩册中启用“加权总成绩”视图,教职员可以配置各标签页的权重并查看加权总成绩列。" - }, - "course.gradebook.ConfigureWeightsPrompt.allExcluded": { - "defaultMessage": "“{tab}”中的所有评估均已排除,且不会计入总成绩。" - }, - "course.gradebook.ConfigureWeightsPrompt.allExcludedCount": { - "defaultMessage": "已排除全部 {n} 个" - }, - "course.gradebook.ConfigureWeightsPrompt.customMode": { - "defaultMessage": "自定义" - }, - "course.gradebook.ConfigureWeightsPrompt.customSum": { - "defaultMessage": "评估权重:{sum} / {total}" - }, - "course.gradebook.ConfigureWeightsPrompt.defaultsHint": { - "defaultMessage": "尚未设置权重。以下为建议默认值,所有标签页均按相同比重计算。保存以确认,或在下方进行调整。" - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionDrop": { - "defaultMessage": "在等权模式下,可选择在求平均分前剔除每位学生得分最低的 N 个评估。" - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionExclusion": { - "defaultMessage": "展开标签页以将个别评估纳入或排除在评分之外。" - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionIntro": { - "defaultMessage": "控制各标签页和评估如何计入每位学生的总成绩。" - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionModes": { - "defaultMessage": "选择“等权”(所有评估平分该标签页的权重)或“自定义”(设置每个评估所占的份额)。" - }, - "course.gradebook.ConfigureWeightsPrompt.descriptionWeights": { - "defaultMessage": "设置每个标签页的权重,即其对总成绩的贡献比例(权重合计应为 100)。" - }, - "course.gradebook.ConfigureWeightsPrompt.equalMode": { - "defaultMessage": "等权" - }, - "course.gradebook.ConfigureWeightsPrompt.excluded": { - "defaultMessage": "已排除" - }, - "course.gradebook.ConfigureWeightsPrompt.excludedCount": { - "defaultMessage": "已排除 {n} 个" - }, - "course.gradebook.ConfigureWeightsPrompt.includeAssessment": { - "defaultMessage": "将 {assessment} 计入成绩" - }, - "course.gradebook.ConfigureWeightsPrompt.modeAria": { - "defaultMessage": "{tab} 权重模式" - }, - "course.gradebook.ConfigureWeightsPrompt.ofGrade": { - "defaultMessage": "占成绩的 {pct}%" - }, - "course.gradebook.ConfigureWeightsPrompt.promptTitle": { - "defaultMessage": "配置贡献比例" - }, - "course.gradebook.ConfigureWeightsPrompt.saveError": { - "defaultMessage": "保存权重失败。请重试。" - }, - "course.gradebook.ConfigureWeightsPrompt.total": { - "defaultMessage": "总计:{sum}%" - }, - "course.gradebook.ConfigureWeightsPrompt.unbalanced": { - "defaultMessage": "保存前,“{tab}”的评估权重合计必须等于该标签页的总权重。" - }, - "course.gradebook.ConfigureWeightsPrompt.valueTooHigh": { - "defaultMessage": "数值最多为 100" - }, - "course.gradebook.ConfigureWeightsPrompt.valueTooLow": { - "defaultMessage": "数值至少为 0" - }, - "course.gradebook.ConfigureWeightsPrompt.weightsDoNotSum": { - "defaultMessage": "权重合计不为 100。仍可保存;总成绩可能不准确。" - }, - "course.gradebook.GradebookIndex.allAssessments": { - "defaultMessage": "全部评估" - }, - "course.gradebook.GradebookIndex.byWeight": { - "defaultMessage": "加权总成绩" - }, - "course.gradebook.GradebookWeightedTable.collapseRow": { - "defaultMessage": "收起 {name}" - }, - "course.gradebook.GradebookWeightedTable.configureWeights": { - "defaultMessage": "配置权重" - }, - "course.gradebook.GradebookWeightedTable.defaultWeights": { - "defaultMessage": "正在显示默认权重——所有标签页比重相同。点击“配置权重”进行自定义设置。" - }, - "course.gradebook.GradebookWeightedTable.defaultWeightsNoAccess": { - "defaultMessage": "正在显示默认权重——在配置权重之前,所有标签页比重相同。" - }, - "course.gradebook.GradebookWeightedTable.displayPercent": { - "defaultMessage": "百分比" - }, - "course.gradebook.GradebookWeightedTable.displayPercentTooltip": { - "defaultMessage": "学生在各标签页所获得的比例。在比重为 20% 的标签页获得 100%,即表示学生获得了该标签页的全部 20 个成绩分。" - }, - "course.gradebook.GradebookWeightedTable.displayPoints": { - "defaultMessage": "分数" - }, - "course.gradebook.GradebookWeightedTable.displayPointsTooltip": { - "defaultMessage": "各标签页贡献的成绩分数。各列相加即为预计总成绩。" - }, - "course.gradebook.GradebookWeightedTable.downloadCsv": { - "defaultMessage": "下载为 CSV" - }, - "course.gradebook.GradebookWeightedTable.email": { - "defaultMessage": "电子邮件" - }, - "course.gradebook.GradebookWeightedTable.excluded": { - "defaultMessage": "已排除" - }, - "course.gradebook.GradebookWeightedTable.expandRow": { - "defaultMessage": "展开 {name}" - }, - "course.gradebook.GradebookWeightedTable.name": { - "defaultMessage": "姓名" - }, - "course.gradebook.GradebookWeightedTable.noWeightsConfigured": { - "defaultMessage": "尚未配置权重——所有标签页的权重均为 0。点击“配置权重”以分配权重。" - }, - "course.gradebook.GradebookWeightedTable.noWeightsNoAccess": { - "defaultMessage": "尚未配置任何标签页权重。" - }, - "course.gradebook.GradebookWeightedTable.outOfWeight": { - "defaultMessage": "/{weight}" - }, - "course.gradebook.GradebookWeightedTable.percentOfGrade": { - "defaultMessage": "占成绩的 {weight}%" - }, - "course.gradebook.GradebookWeightedTable.percentTotalExact": { - "defaultMessage": "合计 100%" - }, - "course.gradebook.GradebookWeightedTable.percentTotalWarning": { - "defaultMessage": "合计 {weight}%" - }, - "course.gradebook.GradebookWeightedTable.total": { - "defaultMessage": "总成绩" - }, - "course.gradebook.GradebookWeightedTable.searchStudents": { - "defaultMessage": "搜索学生" - }, - "course.gradebook.GradebookWeightedTable.weightsDoNotSum": { - "defaultMessage": "权重合计不为 100。总成绩可能不准确。" - }, - "course.gradebook.TotalHint.policy": { - "defaultMessage": "总成绩将未评分的评估按 0 分计算。" - }, - "course.gradebook.WeightedViewHint.hint": { - "defaultMessage": "需要加权总成绩吗?您可以设置每个标签页在每位学生总成绩中所占的比重,并在此查看加权总成绩。请在{link}中启用。" - }, - "course.gradebook.WeightedViewHint.settingsLink": { - "defaultMessage": "成绩册设置" } } diff --git a/config/locales/en/activerecord/errors.yml b/config/locales/en/activerecord/errors.yml index 713858a6dc..c425d123fc 100644 --- a/config/locales/en/activerecord/errors.yml +++ b/config/locales/en/activerecord/errors.yml @@ -6,6 +6,10 @@ en: attributes: reference_timelines: must_have_at_most_one_default: 'must have at most one default' + course/gradebook/contribution: + attributes: + base: + exactly_one_contributor: "must reference exactly one contributor (a tab or an external assessment)" course/announcement: attributes: end_at: diff --git a/config/routes.rb b/config/routes.rb index 0ff051754c..bc3518af92 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -502,6 +502,16 @@ resource :gradebook, only: [] do get '/' => 'gradebook#index' patch '/weights' => 'gradebook#update_weights' + resources :external_assessments, only: [:create, :update, :destroy] do + member do + put 'grades' => 'external_assessments#grades' + end + end + resources :external_assessment_imports, only: [:create] do + collection do + post 'preview' + end + end end scope module: :discussion do diff --git a/db/migrate/20260615000000_create_course_external_assessments_and_grades.rb b/db/migrate/20260615000000_create_course_external_assessments_and_grades.rb new file mode 100644 index 0000000000..28a240a467 --- /dev/null +++ b/db/migrate/20260615000000_create_course_external_assessments_and_grades.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +class CreateCourseExternalAssessmentsAndGrades < ActiveRecord::Migration[7.2] + def change + create_table :course_external_assessments do |t| + t.references :course, null: false, + foreign_key: { to_table: :courses, + name: 'fk_course_external_assessments_course_id' }, + index: { name: 'fk__course_external_assessments_course_id' } + t.string :title, null: false + t.decimal :maximum_grade, precision: 4, scale: 1, null: false + t.references :creator, null: false, + foreign_key: { to_table: :users, + name: 'fk_course_external_assessments_creator_id' }, + index: { name: 'fk__course_external_assessments_creator_id' } + t.references :updater, null: false, + foreign_key: { to_table: :users, + name: 'fk_course_external_assessments_updater_id' }, + index: { name: 'fk__course_external_assessments_updater_id' } + t.timestamps null: false + end + add_index :course_external_assessments, [:course_id, :title], + unique: true, name: 'index_course_external_assessments_on_course_id_and_title' + + create_table :course_external_assessment_grades do |t| + t.references :external_assessment, null: false, + foreign_key: { to_table: :course_external_assessments, + name: 'fk_course_external_assessment_grades_' \ + 'external_assessment_id' }, + index: { name: 'fk__course_external_assessment_grades_external_assessment_id' } + t.references :course_user, null: false, + foreign_key: { to_table: :course_users, + name: 'fk_course_external_assessment_grades_course_user_id' }, + index: { name: 'fk__course_external_assessment_grades_course_user_id' } + t.decimal :grade, precision: 4, scale: 1, null: true + t.string :imported_identifier, null: true + t.references :creator, null: false, + foreign_key: { to_table: :users, name: 'fk_course_external_assessment_grades_creator_id' }, + index: { name: 'fk__course_external_assessment_grades_creator_id' } + t.references :updater, null: false, + foreign_key: { to_table: :users, name: 'fk_course_external_assessment_grades_updater_id' }, + index: { name: 'fk__course_external_assessment_grades_updater_id' } + t.timestamps null: false + end + add_index :course_external_assessment_grades, [:external_assessment_id, :course_user_id], + unique: true, name: 'index_course_external_assessment_grades_on_ea_id_and_cu_id' + end +end diff --git a/db/migrate/20260616000000_add_external_assessment_to_gradebook_contributions.rb b/db/migrate/20260616000000_add_external_assessment_to_gradebook_contributions.rb new file mode 100644 index 0000000000..9bfad2e985 --- /dev/null +++ b/db/migrate/20260616000000_add_external_assessment_to_gradebook_contributions.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +class AddExternalAssessmentToGradebookContributions < ActiveRecord::Migration[7.2] + def change + add_reference :course_gradebook_contributions, :external_assessment, null: true, + foreign_key: { to_table: :course_external_assessments, on_delete: :cascade }, + index: { unique: true, + name: 'index_course_gradebook_contributions_on_external_assessment_id' } + + # Exactly one contributor: either a native tab, or an external assessment. + add_check_constraint :course_gradebook_contributions, + '(tab_id IS NOT NULL) <> (external_assessment_id IS NOT NULL)', + name: 'chk_gradebook_contribution_exactly_one_contributor' + end +end diff --git a/db/schema.rb b/db/schema.rb index d1a22b40e8..b64274d9fa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_06_11_000000) do +ActiveRecord::Schema[7.2].define(version: 2026_06_16_000000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "uuid-ossp" @@ -762,6 +762,36 @@ t.index ["updater_id"], name: "fk__course_experience_points_records_updater_id" end + create_table "course_external_assessment_grades", force: :cascade do |t| + t.bigint "external_assessment_id", null: false + t.bigint "course_user_id", null: false + t.decimal "grade", precision: 4, scale: 1 + t.string "imported_identifier" + t.bigint "creator_id", null: false + t.bigint "updater_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["course_user_id"], name: "fk__course_external_assessment_grades_course_user_id" + t.index ["creator_id"], name: "fk__course_external_assessment_grades_creator_id" + t.index ["external_assessment_id", "course_user_id"], name: "index_course_external_assessment_grades_on_ea_id_and_cu_id", unique: true + t.index ["external_assessment_id"], name: "fk__course_external_assessment_grades_external_assessment_id" + t.index ["updater_id"], name: "fk__course_external_assessment_grades_updater_id" + end + + create_table "course_external_assessments", force: :cascade do |t| + t.bigint "course_id", null: false + t.string "title", null: false + t.decimal "maximum_grade", precision: 4, scale: 1, null: false + t.bigint "creator_id", null: false + t.bigint "updater_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["course_id", "title"], name: "index_course_external_assessments_on_course_id_and_title", unique: true + t.index ["course_id"], name: "fk__course_external_assessments_course_id" + t.index ["creator_id"], name: "fk__course_external_assessments_creator_id" + t.index ["updater_id"], name: "fk__course_external_assessments_updater_id" + end + create_table "course_forum_discussion_references", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false @@ -879,8 +909,11 @@ t.bigint "updater_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "external_assessment_id" + t.check_constraint "(tab_id IS NOT NULL) <> (external_assessment_id IS NOT NULL)", name: "chk_gradebook_contribution_exactly_one_contributor" t.index ["course_id"], name: "fk__course_gradebook_contributions_course_id" t.index ["creator_id"], name: "fk__course_gradebook_contributions_creator_id" + t.index ["external_assessment_id"], name: "index_course_gradebook_contributions_on_external_assessment_id", unique: true t.index ["tab_id"], name: "index_course_gradebook_contributions_on_tab_id", unique: true t.index ["updater_id"], name: "fk__course_gradebook_contributions_updater_id" end @@ -1925,6 +1958,13 @@ add_foreign_key "course_experience_points_records", "users", column: "awarder_id", name: "fk_course_experience_points_records_awarder_id" add_foreign_key "course_experience_points_records", "users", column: "creator_id", name: "fk_course_experience_points_records_creator_id" add_foreign_key "course_experience_points_records", "users", column: "updater_id", name: "fk_course_experience_points_records_updater_id" + add_foreign_key "course_external_assessment_grades", "course_external_assessments", column: "external_assessment_id", name: "fk_course_external_assessment_grades_external_assessment_id" + add_foreign_key "course_external_assessment_grades", "course_users", name: "fk_course_external_assessment_grades_course_user_id" + add_foreign_key "course_external_assessment_grades", "users", column: "creator_id", name: "fk_course_external_assessment_grades_creator_id" + add_foreign_key "course_external_assessment_grades", "users", column: "updater_id", name: "fk_course_external_assessment_grades_updater_id" + add_foreign_key "course_external_assessments", "courses", name: "fk_course_external_assessments_course_id" + add_foreign_key "course_external_assessments", "users", column: "creator_id", name: "fk_course_external_assessments_creator_id" + add_foreign_key "course_external_assessments", "users", column: "updater_id", name: "fk_course_external_assessments_updater_id" add_foreign_key "course_forum_discussion_references", "course_forum_discussions", column: "discussion_id", name: "fk_course_forum_discussion_references_discussion_id" add_foreign_key "course_forum_discussion_references", "course_forum_imports", column: "forum_import_id", name: "fk_course_forum_discussion_references_forum_import_id" add_foreign_key "course_forum_discussion_references", "users", column: "creator_id", name: "fk_course_forum_discussion_references_creator_id" @@ -1948,6 +1988,7 @@ add_foreign_key "course_gradebook_assessment_contributions", "users", column: "creator_id" add_foreign_key "course_gradebook_assessment_contributions", "users", column: "updater_id" add_foreign_key "course_gradebook_contributions", "course_assessment_tabs", column: "tab_id", on_delete: :cascade + add_foreign_key "course_gradebook_contributions", "course_external_assessments", column: "external_assessment_id", on_delete: :cascade add_foreign_key "course_gradebook_contributions", "courses" add_foreign_key "course_gradebook_contributions", "users", column: "creator_id" add_foreign_key "course_gradebook_contributions", "users", column: "updater_id" diff --git a/spec/controllers/course/external_assessment_imports_controller_spec.rb b/spec/controllers/course/external_assessment_imports_controller_spec.rb new file mode 100644 index 0000000000..6020f61092 --- /dev/null +++ b/spec/controllers/course/external_assessment_imports_controller_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe Course::ExternalAssessmentImportsController, type: :controller do + let(:instance) { Instance.default } + + with_tenant(:instance) do + let(:course) { create(:course) } + let(:manager) { create(:course_manager, course: course) } + let(:ta) { create(:course_teaching_assistant, course: course) } + let!(:alice) { create(:course_student, course: course, external_id: 'A001') } + let!(:bob) { create(:course_student, course: course, external_id: 'A002') } + + let(:components) { [name: 'Midterm', weightage: 30, maximumGrade: 50] } + let(:csv_data) { "Identifier,Midterm\nA001,41\n" } + let(:base_params) do + { course_id: course.id, format: :json, + components: components, identifierMode: 'student_id', csvData: csv_data } + end + + describe '#preview' do + render_views + context 'as a manager' do + before { controller_sign_in(controller, manager.user) } + + it 'returns ok with a sample and writes nothing' do + expect { post :preview, params: base_params }. + not_to(change { Course::ExternalAssessmentGrade.count }) + data = JSON.parse(response.body) + expect(data['ok']).to be(true) + expect(data['sample'].first['studentName']).to eq(alice.name) + end + + it 'returns ok:false with unresolved identifiers' do + post :preview, params: base_params.merge(csvData: "Identifier,Midterm\nZZZ,1\n") + data = JSON.parse(response.body) + expect(data['ok']).to be(false) + expect(data['unresolved']).to include('ZZZ') + end + + it 'returns 422 on a malformed header' do + post :preview, params: base_params.merge(csvData: "Wrong,Midterm\nA001,1\n") + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns conflicts when a grade already exists' do + # Seed an existing grade for alice + service = Course::Gradebook::ExternalAssessmentImportService.new( + course: course, actor: manager.user, + components: [name: 'Midterm', weightage: 30, maximum_grade: 50], + identifier_mode: 'student_id', csv_data: "Identifier,Midterm\nA001,10\n" + ) + service.commit(on_conflict: 'replace') + + post :preview, params: base_params.merge(csvData: "Identifier,Midterm\nA001,20\n") + data = JSON.parse(response.body) + expect(data['conflicts'].size).to eq(1) + expect(data['conflicts'].first['component']).to eq('Midterm') + end + + it 'returns ok:false with malformed grade cells' do + post :preview, params: base_params.merge(csvData: "Identifier,Midterm\nA001,oops\n") + data = JSON.parse(response.body) + expect(data['ok']).to be(false) + expect(data['malformed']).to be_present + end + + it 'returns 422 on duplicate component names' do + dup_components = [{ name: 'Midterm', weightage: 30, maximumGrade: 50 }, + { name: 'Midterm', weightage: 20, maximumGrade: 40 }] + post :preview, params: base_params.merge( + components: dup_components, + csvData: "Identifier,Midterm,Midterm\nA001,1,2\n" + ) + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'resolves by email when identifierMode is email' do + post :preview, params: base_params.merge( + identifierMode: 'email', + csvData: "Identifier,Midterm\n#{alice.user.email},41\n" + ) + data = JSON.parse(response.body) + expect(data['ok']).to be(true) + expect(data['sample'].first['studentName']).to eq(alice.name) + end + end + + context 'as a teaching assistant' do + before { controller_sign_in(controller, ta.user) } + + it 'is denied' do + expect { post :preview, params: base_params }.to raise_error(CanCan::AccessDenied) + end + end + end + + describe '#create (commit)' do + render_views + context 'as a manager' do + before { controller_sign_in(controller, manager.user) } + + it 'commits and returns a summary' do + expect { post :create, params: base_params.merge(onConflict: 'replace') }. + to change { Course::ExternalAssessmentGrade.count }.by(1) + data = JSON.parse(response.body) + expect(data['createdComponents']).to eq(1) + expect(data['gradesWritten']).to eq(1) + end + + it 'returns 422 and writes nothing on an unresolved identifier' do + expect do + post :create, params: base_params.merge( + csvData: "Identifier,Midterm\nZZZ,1\n", onConflict: 'replace' + ) + end.not_to(change { Course::ExternalAssessmentGrade.count }) + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'commits with onConflict keep and returns updatedComponents' do + # Seed first + post :create, params: base_params.merge(onConflict: 'replace') + # Re-import with keep + expect do + post :create, params: base_params.merge(onConflict: 'keep', + csvData: "Identifier,Midterm\nA001,99\n") + end.not_to(change { Course::ExternalAssessmentGrade.count }) + data = JSON.parse(response.body) + expect(data['updatedComponents']).to eq(1) + expect(data['createdComponents']).to eq(0) + end + + it 'returns 422 and writes nothing on malformed grade cells' do + expect do + post :create, params: base_params.merge( + csvData: "Identifier,Midterm\nA001,oops\n", onConflict: 'replace' + ) + end.not_to(change { Course::ExternalAssessmentGrade.count }) + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'as a teaching assistant' do + before { controller_sign_in(controller, ta.user) } + + it 'is denied' do + expect { post :create, params: base_params.merge(onConflict: 'keep') }. + to raise_error(CanCan::AccessDenied) + end + end + end + end +end diff --git a/spec/controllers/course/external_assessments_controller_spec.rb b/spec/controllers/course/external_assessments_controller_spec.rb new file mode 100644 index 0000000000..f710d86bfd --- /dev/null +++ b/spec/controllers/course/external_assessments_controller_spec.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe Course::ExternalAssessmentsController, type: :controller do + let(:instance) { Instance.default } + + with_tenant(:instance) do + let(:course) { create(:course) } + let(:manager) { create(:course_manager, course: course) } + let(:ta) { create(:course_teaching_assistant, course: course) } + + describe '#create' do + render_views + let(:params) do + { course_id: course.id, format: :json, title: 'Final', maximumGrade: 100 } + end + + context 'as a manager' do + before { controller_sign_in(controller, manager.user) } + + it 'creates an external assessment with a contribution and no tab/category' do + expect do + post :create, params: params + end.to change(Course::ExternalAssessment, :count).by(1). + and change(Course::Gradebook::Contribution, :count).by(1). + and not_change(Course::Assessment::Tab, :count) + expect(response).to be_successful + body = JSON.parse(response.body) + created = Course::ExternalAssessment.last + expect(body['assessment']['id']).to eq(-created.id) + expect(body['assessment']['id']).to be < 0 + expect(body['assessment']['title']).to eq('Final') + expect(body['assessment']['maxGrade']).to eq(100.0) + expect(body['assessment']['external']).to be(true) + expect(body['assessment']['gradebookExcluded']).to be(false) + expect(body['tab']['id']).to eq(body['assessment']['tabId']) + expect(body['tab']['id']).to be < 0 + expect(body['tab']['categoryId']).to eq(Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID) + expect(body['category']['id']).to eq(Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID) + expect(body['category']['title']).to eq(Course::ExternalAssessment::SYNTHETIC_CATEGORY_TITLE) + end + + it 'returns 422 on a blank title' do + post :create, params: params.merge(title: '') + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns 422 on a blank maximumGrade' do + post :create, params: params.merge(maximumGrade: '') + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'as a teaching assistant' do + before { controller_sign_in(controller, ta.user) } + + it 'is denied' do + expect { post :create, params: params }.to raise_error(CanCan::AccessDenied) + end + end + end + + describe '#update' do + render_views + let!(:external) { create(:course_external_assessment, course: course, title: 'Mid') } + + context 'as a manager' do + before { controller_sign_in(controller, manager.user) } + + it 'renames and changes the maximum grade without touching any tab' do + expect do + patch :update, params: { course_id: course.id, id: external.id, format: :json, + title: 'Midterm', maximumGrade: 60 } + end.not_to change(Course::Assessment::Tab, :count) + expect(response).to be_successful + body = JSON.parse(response.body) + expect(body['assessment']['id']).to eq(-external.id) + expect(body['assessment']['title']).to eq('Midterm') + expect(body['assessment']['maxGrade']).to eq(60.0) + expect(body['assessment']['external']).to be(true) + expect(body['assessment']['gradebookExcluded']).to be(false) + expect(body['tab']['id']).to eq(-external.id) + expect(body['tab']['title']).to eq('Midterm') + expect(body['tab']['categoryId']).to eq(Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID) + expect(external.reload.title).to eq('Midterm') + expect(external.maximum_grade).to eq(60) + end + + it 'returns 404 when the external belongs to another course' do + other_external = create(:course_external_assessment) + patch :update, params: { course_id: course.id, id: other_external.id, format: :json, title: 'X' } + expect(response).to have_http_status(:not_found) + end + + it 'returns 422 on a blank title' do + patch :update, params: { course_id: course.id, id: external.id, format: :json, + title: '' } + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context 'as a teaching assistant' do + before { controller_sign_in(controller, ta.user) } + + it 'is denied' do + expect do + patch :update, params: { course_id: course.id, id: external.id, format: :json, title: 'X' } + end.to raise_error(CanCan::AccessDenied) + end + end + end + + describe '#destroy' do + let!(:external) { create(:course_external_assessment, course: course) } + + context 'as a manager' do + before { controller_sign_in(controller, manager.user) } + + it 'deletes the external and cascades grades' do + create(:course_external_assessment_grade, external_assessment: external) + expect do + delete :destroy, params: { course_id: course.id, id: external.id, format: :json } + end.to change { Course::ExternalAssessment.count }.by(-1). + and change { Course::ExternalAssessmentGrade.count }.by(-1) + expect(response).to be_successful + end + + it 'returns 404 when the external belongs to another course' do + other_external = create(:course_external_assessment) + expect do + delete :destroy, params: { course_id: course.id, id: other_external.id, format: :json } + end.not_to(change { Course::ExternalAssessment.count }) + expect(response).to have_http_status(:not_found) + end + end + + context 'as a teaching assistant' do + before { controller_sign_in(controller, ta.user) } + + it 'is denied' do + expect { delete :destroy, params: { course_id: course.id, id: external.id, format: :json } }. + to raise_error(CanCan::AccessDenied) + end + end + end + + describe '#grades' do + render_views + let!(:external) { create(:course_external_assessment, course: course) } + let(:gb_student) { create(:course_student, course: course) } + + context 'as a teaching assistant (grading-capable staff)' do + before { controller_sign_in(controller, ta.user) } + + it 'inserts a grade for a student who has none' do + expect do + put :grades, params: { course_id: course.id, id: external.id, format: :json, + courseUserId: gb_student.id, grade: 88 } + end.to change { Course::ExternalAssessmentGrade.count }.by(1) + expect(response).to be_successful + data = JSON.parse(response.body) + expect(data['studentId']).to eq(gb_student.user_id) + expect(data['assessmentId']).to eq(-external.id) + expect(data['grade']).to eq(88.0) + end + + it 'updates an existing grade in place (no duplicate row)' do + grade = create(:course_external_assessment_grade, + external_assessment: external, course_user: gb_student, grade: 10) + expect do + put :grades, params: { course_id: course.id, id: external.id, format: :json, + courseUserId: gb_student.id, grade: 20 } + end.not_to(change { Course::ExternalAssessmentGrade.count }) + expect(grade.reload.grade).to eq(20) + end + + it 'clears a grade to null (ungraded) when grade is blank' do + grade = create(:course_external_assessment_grade, + external_assessment: external, course_user: gb_student, grade: 10) + put :grades, params: { course_id: course.id, id: external.id, format: :json, + courseUserId: gb_student.id, grade: '' } + expect(grade.reload.grade).to be_nil + end + + it 'returns 404 when the courseUserId does not belong to the course' do + other_student = create(:course_student) + put :grades, params: { course_id: course.id, id: external.id, format: :json, + courseUserId: other_student.id, grade: 50 } + expect(response).to have_http_status(:not_found) + end + end + + context 'as a student' do + let(:viewer) { create(:course_student, course: course) } + before { controller_sign_in(controller, viewer.user) } + + it 'is denied' do + expect do + put :grades, params: { course_id: course.id, id: external.id, format: :json, + courseUserId: gb_student.id, grade: 5 } + end.to raise_error(CanCan::AccessDenied) + end + end + end + end +end diff --git a/spec/controllers/course/gradebook_controller_spec.rb b/spec/controllers/course/gradebook_controller_spec.rb index 59397f4120..74d54d758f 100644 --- a/spec/controllers/course/gradebook_controller_spec.rb +++ b/spec/controllers/course/gradebook_controller_spec.rb @@ -199,6 +199,114 @@ expect(sub['grade']).to be_nil end end + + context 'when the course has an external assessment' do + render_views + let(:ta) { create(:course_teaching_assistant, course: course) } + let(:gb_student) { create(:course_student, course: course) } + let!(:external) do + create(:course_external_assessment, course: course, title: 'Midterm', maximum_grade: 50) + end + let!(:external_grade) do + create(:course_external_assessment_grade, + external_assessment: external, course_user: gb_student, grade: 41) + end + before { controller_sign_in(controller, ta.user) } + + it 'merges the external into assessments with a negative id and external flag' do + subject + data = JSON.parse(response.body) + ext_row = data['assessments'].find { |a| a['id'] == -external.id } + expect(ext_row).to be_present + expect(ext_row['title']).to eq('Midterm') + expect(ext_row['external']).to be(true) + expect(ext_row['maxGrade']).to eq(50.0) + expect(ext_row['tabId']).to eq(external.synthetic_tab_id) + end + + it 'merges the external grade into submissions with a negative assessmentId' do + subject + data = JSON.parse(response.body) + sub = data['submissions'].find { |s| s['assessmentId'] == -external.id } + expect(sub).to be_present + expect(sub['studentId']).to eq(gb_student.user_id) + expect(sub['grade']).to eq(41.0) + end + + it 'emits a synthetic External Assessments category' do + subject + data = JSON.parse(response.body) + cat = data['categories'].find { |c| c['id'] == Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID } + expect(cat).to be_present + expect(cat['title']).to eq('External Assessments') + end + + it 'emits a synthetic tab with negative id under the synthetic category' do + subject + data = JSON.parse(response.body) + tab = data['tabs'].find { |t| t['id'] == external.synthetic_tab_id } + expect(tab).to be_present + expect(tab['categoryId']).to eq(Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID) + end + + it 'creates no real tab or category for the external' do + tab_count_before = Course::Assessment::Tab.count + cat_count_before = Course::Assessment::Category.count + subject + expect(Course::Assessment::Tab.count).to eq(tab_count_before) + expect(Course::Assessment::Category.count).to eq(cat_count_before) + expect(Course::Assessment::Category.where(title: 'External Assessments')).to be_empty + end + end + end + + describe 'GET #index with externals' do + render_views + let!(:course) { create(:course) } + let!(:external) do + Course::ExternalAssessment.create_for_course!(course: course, title: 'Midterm', + maximum_grade: 50.0, weight: 40) + end + let(:ta) { create(:course_teaching_assistant, course: course) } + + before do + ctx = Struct.new(:current_course, :key).new(course, Course::GradebookComponent.key) + Course::Settings::GradebookComponent.new(ctx).weighted_view_enabled = true + course.save! + controller_sign_in(controller, ta.user) + end + + subject(:body) do + get(:index, params: { course_id: course }, format: :json) + JSON.parse(response.body) + end + + it 'emits a synthetic External Assessments category' do + cat = body['categories'].find { |c| c['id'] == Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID } + expect(cat['title']).to eq('External Assessments') + end + + it 'emits one synthetic tab per external carrying its weight' do + tab = body['tabs'].find { |t| t['id'] == -external.id } + expect(tab['categoryId']).to eq(Course::ExternalAssessment::SYNTHETIC_CATEGORY_ID) + expect(tab['gradebookWeight']).to eq(40.0) + expect(tab['weightMode']).to eq('equal') + end + + it 'emits the external as a negative-id leaf under its synthetic tab' do + leaf = body['assessments'].find { |a| a['id'] == -external.id } + expect(leaf['external']).to be(true) + expect(leaf['tabId']).to eq(-external.id) + end + + it 'creates no real tab or category for the external' do + tab_count_before = Course::Assessment::Tab.count + cat_count_before = Course::Assessment::Category.count + body + expect(Course::Assessment::Tab.count).to eq(tab_count_before) + expect(Course::Assessment::Category.count).to eq(cat_count_before) + expect(Course::Assessment::Category.where(title: 'External Assessments')).to be_empty + end end describe 'PATCH update_weights' do @@ -310,13 +418,13 @@ def weight_for(tab) it 'persists custom mode + assessment weights and echoes them back' do post :update_weights, as: :json, params: { course_id: course.id, - weights: [{ + weights: [ tabId: tab.id, weight: '50', weightMode: 'custom', assessmentWeights: [ { assessmentId: a1.id, weight: '30' }, { assessmentId: a2.id, weight: '20' } ] - }] + ] } expect(response).to have_http_status(:ok) body = JSON.parse(response.body) @@ -332,10 +440,10 @@ def weight_for(tab) it 'returns 422 when custom weights do not sum to the tab total' do post :update_weights, as: :json, params: { course_id: course.id, - weights: [{ + weights: [ tabId: tab.id, weight: '50', weightMode: 'custom', - assessmentWeights: [{ assessmentId: a1.id, weight: '10' }] - }] + assessmentWeights: [assessmentId: a1.id, weight: '10'] + ] } expect(response).to have_http_status(:unprocessable_entity) end @@ -343,10 +451,10 @@ def weight_for(tab) it 'persists and echoes per-assessment exclusion in equal mode' do post :update_weights, as: :json, params: { course_id: course.id, - weights: [{ + weights: [ tabId: tab.id, weight: '50', weightMode: 'equal', excludedAssessmentIds: [a1.id] - }] + ] } expect(response).to have_http_status(:ok) expect(a1.reload.gradebook_assessment_contribution.excluded).to eq(true) diff --git a/spec/factories/course_external_assessments.rb b/spec/factories/course_external_assessments.rb new file mode 100644 index 0000000000..5aa07599ba --- /dev/null +++ b/spec/factories/course_external_assessments.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +FactoryBot.define do + factory :course_external_assessment, class: Course::ExternalAssessment do + course + sequence(:title) { |n| "External #{n}" } + maximum_grade { 100.0 } + end + + factory :course_external_assessment_grade, class: Course::ExternalAssessmentGrade do + external_assessment { association(:course_external_assessment) } + course_user { association(:course_user) } + grade { 50.0 } + end +end diff --git a/spec/factories/course_gradebook_contributions.rb b/spec/factories/course_gradebook_contributions.rb index 606994f4aa..94ff157305 100644 --- a/spec/factories/course_gradebook_contributions.rb +++ b/spec/factories/course_gradebook_contributions.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true FactoryBot.define do - factory :course_gradebook_contribution, class: Course::Gradebook::Contribution.name do - association :tab, factory: :course_assessment_tab - course { tab.category.course } + factory :course_gradebook_contribution, class: Course::Gradebook::Contribution do + course + tab { association(:course_assessment_tab, course: course) } weight { 0 } weight_mode { :equal } + keep_highest { 0 } end end diff --git a/spec/models/course/external_assessment_grade_spec.rb b/spec/models/course/external_assessment_grade_spec.rb new file mode 100644 index 0000000000..6e88b3c1b0 --- /dev/null +++ b/spec/models/course/external_assessment_grade_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe Course::ExternalAssessmentGrade, type: :model do + let(:instance) { Instance.default } + + with_tenant(:instance) do + describe 'validations' do + subject { build(:course_external_assessment_grade) } + + it { is_expected.to be_valid } + + it 'allows a null grade (ungraded)' do + subject.grade = nil + expect(subject).to be_valid + end + + it 'allows a grade greater than the maximum (no ceiling, bonus-consistent)' do + subject.external_assessment.maximum_grade = 10 + subject.grade = 15 + expect(subject).to be_valid + end + + it 'enforces one grade per (external_assessment, course_user)' do + existing = create(:course_external_assessment_grade) + duplicate = build(:course_external_assessment_grade, + external_assessment: existing.external_assessment, + course_user: existing.course_user) + expect(duplicate).not_to be_valid + end + + it 'requires a course_user' do + subject.course_user = nil + expect(subject).not_to be_valid + end + + it 'rejects a non-numeric grade string' do + subject.grade = 'abc' + expect(subject).not_to be_valid + end + + it 'requires an external_assessment' do + subject.external_assessment = nil + expect(subject).not_to be_valid + end + end + + describe 'determinacy — grade binds to course_user, not the identifier string' do + it 'does not move an existing grade when the student external_id changes after import' do + grade = create(:course_external_assessment_grade, imported_identifier: 'A0001X', grade: 5) + course_user = grade.course_user + + course_user.update!(external_id: 'A9999Z') + + expect(grade.reload.course_user_id).to eq(course_user.id) + expect(grade.grade).to eq(5) + end + end + end +end diff --git a/spec/models/course/external_assessment_spec.rb b/spec/models/course/external_assessment_spec.rb new file mode 100644 index 0000000000..dd9d2608b2 --- /dev/null +++ b/spec/models/course/external_assessment_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe Course::ExternalAssessment, type: :model do + let(:instance) { Instance.default } + + with_tenant(:instance) do + let(:course) { create(:course) } + + describe 'associations' do + it { is_expected.to belong_to(:course) } + it { is_expected.to have_one(:gradebook_contribution).dependent(:destroy) } + it { is_expected.to have_many(:external_assessment_grades).dependent(:destroy) } + end + + describe 'validations' do + subject { build(:course_external_assessment, course: course) } + it { is_expected.to validate_presence_of(:title) } + + it 'enforces course-scoped unique titles' do + create(:course_external_assessment, course: course, title: 'Midterm') + dup = build(:course_external_assessment, course: course, title: 'Midterm') + expect(dup).not_to be_valid + expect(dup.errors[:title]).to include(I18n.t('errors.messages.taken')) + end + + it 'allows the same title in different courses' do + create(:course_external_assessment, course: course, title: 'Midterm') + other = build(:course_external_assessment, course: create(:course), title: 'Midterm') + expect(other).to be_valid + end + end + + describe '.create_for_course!' do + it 'creates the external and its contribution row' do + external = nil + expect do + external = described_class.create_for_course!(course: course, title: 'Final', + maximum_grade: 80.0, weight: 30) + end.to change(Course::Gradebook::Contribution, :count).by(1) + expect(external.course).to eq(course) + expect(external.gradebook_contribution.weight).to eq(30) + expect(external.gradebook_contribution.weight_mode).to eq('equal') + end + + it 'does not create any assessment tab or category' do + course # ensure course (and its default tab/category) is created before measuring + expect do + described_class.create_for_course!(course: course, title: 'Final', maximum_grade: 80.0) + end.to not_change(Course::Assessment::Tab, :count).and not_change(Course::Assessment::Category, :count) + end + + it 'raises on duplicate title within the course' do + described_class.create_for_course!(course: course, title: 'Final', maximum_grade: 80.0) + expect do + described_class.create_for_course!(course: course, title: 'Final', maximum_grade: 80.0) + end.to raise_error(ActiveRecord::RecordInvalid) + end + end + + describe '.for_course' do + it 'returns only externals in the course' do + mine = create(:course_external_assessment, course: course) + create(:course_external_assessment, course: create(:course)) + expect(described_class.for_course(course)).to contain_exactly(mine) + end + end + end +end diff --git a/spec/models/course/gradebook/contribution_spec.rb b/spec/models/course/gradebook/contribution_spec.rb index d4d433e028..b818d9ec22 100644 --- a/spec/models/course/gradebook/contribution_spec.rb +++ b/spec/models/course/gradebook/contribution_spec.rb @@ -62,6 +62,78 @@ end end + describe 'external contributor' do + let(:course) { create(:course) } + let(:external) { create(:course_external_assessment, course: course) } + + it 'accepts an external assessment as the sole contributor' do + contribution = build(:course_gradebook_contribution, course: course, + tab: nil, external_assessment: external) + expect(contribution).to be_valid + end + + it 'rejects a row with both a tab and an external assessment' do + tab = create(:course_assessment_tab, course: course) + contribution = build(:course_gradebook_contribution, course: course, + tab: tab, external_assessment: external) + expect(contribution).not_to be_valid + key = 'activerecord.errors.models.course/gradebook/contribution.attributes.base.exactly_one_contributor' + expect(contribution.errors[:base]).to include(I18n.t(key)) + end + + it 'rejects a row with neither contributor' do + contribution = build(:course_gradebook_contribution, course: course, + tab: nil, external_assessment: nil) + expect(contribution).not_to be_valid + end + + it 'validates the external belongs to the same course' do + other = create(:course_external_assessment, course: create(:course)) + contribution = build(:course_gradebook_contribution, course: course, + tab: nil, external_assessment: other) + expect(contribution).not_to be_valid + expect(contribution.errors[:course]).to be_present + end + + it 'cascades on external delete' do + create(:course_gradebook_contribution, course: course, + tab: nil, external_assessment: external) + expect { external.destroy! }.to change(described_class, :count).by(-1) + end + end + + describe '.bulk_update with externals' do + let(:course) { create(:course) } + let!(:external) do + Course::ExternalAssessment.create_for_course!(course: course, title: 'Midterm', + maximum_grade: 50.0, weight: 0) + end + + it 'upserts the external contribution from a negative-id entry' do + described_class.bulk_update(course: course, updates: [ + tab_id: -external.id, weight: 35.0, weight_mode: 'equal' + ]) + expect(external.gradebook_contribution.reload.weight).to eq(35.0) + end + + it 'raises if the external is not in the course' do + other = create(:course_external_assessment, course: create(:course)) + expect do + described_class.bulk_update(course: course, updates: [ + tab_id: -other.id, weight: 10.0, weight_mode: 'equal' + ]) + end.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'does not create assessment-contribution rows for externals' do + expect do + described_class.bulk_update(course: course, updates: [ + tab_id: -external.id, weight: 35.0, weight_mode: 'equal' + ]) + end.not_to change(Course::Gradebook::AssessmentContribution, :count) + end + end + describe '.bulk_update' do let(:tab1) { create(:course_assessment_tab, category: category) } let(:tab2) { create(:course_assessment_tab, category: category) } diff --git a/spec/services/course/gradebook/external_assessment_import_service_spec.rb b/spec/services/course/gradebook/external_assessment_import_service_spec.rb new file mode 100644 index 0000000000..b55eeb9be1 --- /dev/null +++ b/spec/services/course/gradebook/external_assessment_import_service_spec.rb @@ -0,0 +1,273 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe Course::Gradebook::ExternalAssessmentImportService, type: :service do + let(:instance) { Instance.default } + + with_tenant(:instance) do + let(:course) { create(:course) } + let(:actor) { create(:course_manager, course: course).user } + let!(:alice) { create(:course_student, course: course, external_id: 'A001') } + let!(:bob) { create(:course_student, course: course, external_id: 'A002') } + + def service(csv_data:, components:, identifier_mode: 'student_id') + described_class.new( + course: course, actor: actor, components: components, + identifier_mode: identifier_mode, csv_data: csv_data + ) + end + + let(:components) { [name: 'Midterm', weightage: 30, maximum_grade: 50] } + + describe '#preview' do + it 'writes nothing (dry-run)' do + csv = "Identifier,Midterm\nA001,41\nA002,37\n" + expect { service(csv_data: csv, components: components).preview }. + not_to(change { Course::ExternalAssessmentGrade.count }) + end + + it 'returns ok with the first 5 resolved rows (student names)' do + csv = "Identifier,Midterm\nA001,41\nA002,37\n" + result = service(csv_data: csv, components: components).preview + expect(result[:ok]).to be(true) + expect(result[:unresolved]).to be_empty + expect(result[:sample].size).to eq(2) + expect(result[:sample].map { |r| r[:studentName] }).to include(alice.name, bob.name) + expect(result[:sample].first[:grades]['Midterm']).to eq(41.0) + end + + it 'resolves by email when in email mode' do + csv = "Identifier,Midterm\n#{alice.user.email},41\n" + result = service(csv_data: csv, components: components, identifier_mode: 'email').preview + expect(result[:ok]).to be(true) + expect(result[:sample].first[:studentName]).to eq(alice.name) + end + + it 'fails the whole batch on any unresolved identifier' do + csv = "Identifier,Midterm\nA001,41\nZZZZ,37\n" + result = service(csv_data: csv, components: components).preview + expect(result[:ok]).to be(false) + expect(result[:unresolved]).to include('ZZZZ') + end + + it 'flags a malformed (non-numeric) cell' do + csv = "Identifier,Midterm\nA001,oops\n" + result = service(csv_data: csv, components: components).preview + expect(result[:ok]).to be(false) + expect(result[:malformed]).to be_present + end + + it 'rejects an in-file duplicate component name' do + dup = [{ name: 'Midterm', weightage: 30, maximum_grade: 50 }, + { name: 'Midterm', weightage: 20, maximum_grade: 40 }] + csv = "Identifier,Midterm,Midterm\nA001,1,2\n" + expect { service(csv_data: csv, components: dup).preview }. + to raise_error(described_class::ImportError) + end + + it 'raises ImportError on wrong CSV header' do + csv = "Wrong,Midterm\nA001,41\n" + expect { service(csv_data: csv, components: components).preview }. + to raise_error(described_class::ImportError) + end + + it 'returns ok with empty sample when CSV has no data rows' do + csv = "Identifier,Midterm\n" + result = service(csv_data: csv, components: components).preview + expect(result[:ok]).to be(true) + expect(result[:sample]).to be_empty + expect(result[:conflicts]).to be_empty + end + + it 'resolves by email case-insensitively' do + csv = "Identifier,Midterm\n#{alice.user.email.upcase},41\n" + result = service(csv_data: csv, components: components, identifier_mode: 'email').preview + expect(result[:ok]).to be(true) + expect(result[:sample].first[:studentName]).to eq(alice.name) + end + + it 'deduplicates unresolved identifiers' do + csv = "Identifier,Midterm\nZZZZ,1\nZZZZ,2\n" + result = service(csv_data: csv, components: components).preview + expect(result[:unresolved].count('ZZZZ')).to eq(1) + end + + it 'treats a blank cell as ungraded in the sample' do + csv = "Identifier,Midterm\nA001,\n" + result = service(csv_data: csv, components: components).preview + expect(result[:sample].first[:grades]['Midterm']).to be_nil + end + end + + describe '#commit (fresh import)' do + let(:components) { [name: 'Midterm', weightage: 30, maximum_grade: 50] } + + it 'creates the external in the External Assessments category with the typed weight' do + csv = "Identifier,Midterm\nA001,41\nA002,37\n" + summary = service(csv_data: csv, components: components).commit(on_conflict: 'replace') + external = Course::ExternalAssessment.for_course(course).find_by(title: 'Midterm') + expect(external).to be_present + expect(external.maximum_grade).to eq(50) + expect(external.gradebook_contribution.weight).to eq(30) + expect(summary[:createdComponents]).to eq(1) + expect(summary[:gradesWritten]).to eq(2) + end + + it 'writes one grade row per resolved student bound to course_user' do + csv = "Identifier,Midterm\nA001,41\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + external = Course::ExternalAssessment.for_course(course).find_by!(title: 'Midterm') + grade = external.external_assessment_grades.find_by!(course_user: alice) + expect(grade.course_user_id).to eq(alice.id) + expect(grade.grade).to eq(41) + expect(grade.imported_identifier).to eq('A001') + end + + it 'skips a blank cell on a fresh import (no grade row created)' do + csv = "Identifier,Midterm\nA001,\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + # After fix: blank cell on fresh import does NOT create a grade row (filter_map skips nil) + external = Course::ExternalAssessment.for_course(course).find_by(title: 'Midterm') + expect(external.external_assessment_grades.count).to eq(0) + end + + it 'accepts a grade greater than the max (no ceiling)' do + csv = "Identifier,Midterm\nA001,60\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + external = Course::ExternalAssessment.for_course(course).find_by!(title: 'Midterm') + expect(external.external_assessment_grades.find_by!(course_user: alice).grade).to eq(60) + end + + it 'creates multiple components as separate externals' do + comps = [{ name: 'Midterm', weightage: 30, maximum_grade: 50 }, + { name: 'Final', weightage: 50, maximum_grade: 100 }] + csv = "Identifier,Midterm,Final\nA001,40,80\n" + service(csv_data: csv, components: comps).commit(on_conflict: 'replace') + expect(Course::ExternalAssessment.for_course(course).pluck(:title)).to contain_exactly('Midterm', 'Final') + expect(Course::ExternalAssessment.for_course(course).find_by!(title: 'Midterm'). + external_assessment_grades.count).to eq(1) + expect(Course::ExternalAssessment.for_course(course).find_by!(title: 'Final'). + external_assessment_grades.count).to eq(1) + end + + it 'writes nothing when an identifier does not resolve' do + csv = "Identifier,Midterm\nA001,41\nZZZ,9\n" + expect do + expect do + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + end.to raise_error(described_class::ImportError) + end.not_to(change { Course::ExternalAssessmentGrade.count }) + end + end + + describe '#commit (upsert into existing component)' do + let(:components) { [name: 'Midterm', weightage: 30, maximum_grade: 50] } + + def seed_initial! + csv = "Identifier,Midterm\nA001,10\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + Course::ExternalAssessment.for_course(course).find_by(title: 'Midterm') + end + + it 'updates grades into the same component (no second tab)' do + external = seed_initial! + csv = "Identifier,Midterm\nA001,20\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + expect(Course::ExternalAssessment.for_course(course).where(title: 'Midterm').count).to eq(1) + expect(external.external_assessment_grades.find_by(course_user: alice).grade).to eq(20) + end + + it "keeps existing grades when on_conflict is 'keep'" do + external = seed_initial! + csv = "Identifier,Midterm\nA001,99\n" + service(csv_data: csv, components: components).commit(on_conflict: 'keep') + expect(external.external_assessment_grades.find_by(course_user: alice).grade).to eq(10) + end + + it 'inserts a grade for a brand-new student regardless of on_conflict' do + external = seed_initial! + csv = "Identifier,Midterm\nA002,55\n" + service(csv_data: csv, components: components).commit(on_conflict: 'keep') + expect(external.external_assessment_grades.find_by(course_user: bob).grade).to eq(55) + end + + it 'skips a blank cell on upsert (existing grade unchanged)' do + external = seed_initial! + csv = "Identifier,Midterm\nA001,\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + expect(external.external_assessment_grades.find_by(course_user: alice).grade).to eq(10) + end + + it 'never changes the external max or contribution weight on upsert' do + external = seed_initial! + csv = "Identifier,Midterm\nA001,20\n" + comps = [name: 'Midterm', weightage: 99, maximum_grade: 999] + service(csv_data: csv, components: comps).commit(on_conflict: 'replace') + expect(external.reload.maximum_grade).to eq(50) + expect(external.gradebook_contribution.reload.weight).to eq(30) + end + + it 'lists conflicts only for existing non-blank grade rows' do + seed_initial! + csv = "Identifier,Midterm\nA001,20\nA002,33\n" + result = service(csv_data: csv, components: components).preview + expect(result[:conflicts].map { |c| c[:studentName] }).to contain_exactly(alice.name) + conflict = result[:conflicts].first + expect(conflict[:existingGrade]).to eq(10.0) + expect(conflict[:inFileGrade]).to eq(20.0) + end + + it 'flags identifierMismatch when a row resolves to a student imported under a different id' do + seed_initial! # alice imported under 'A001' + bob.update!(external_id: 'A777') + alice.update!(external_id: 'A002') # alice now owns A002 (formerly bob's) + csv = "Identifier,Midterm\nA002,20\n" # A002 now → alice, but her grade was imported as 'A001' + result = service(csv_data: csv, components: components).preview + mismatch = result[:conflicts].find { |c| c[:studentName] == alice.name } + expect(mismatch[:identifierMismatch]).to be(true) + end + + it 'returns updatedComponents: 1 after an upsert' do + seed_initial! + csv = "Identifier,Midterm\nA001,20\n" + summary = service(csv_data: csv, components: components).commit(on_conflict: 'replace') + expect(summary[:updatedComponents]).to eq(1) + expect(summary[:createdComponents]).to eq(0) + end + + it 'updates a nil existing grade even when on_conflict is keep' do + external = seed_initial! + # Manually clear the grade to nil (simulates a partial import that wrote the row but not the value) + external.external_assessment_grades.find_by(course_user: alice).update_column(:grade, nil) + csv = "Identifier,Midterm\nA001,50\n" + service(csv_data: csv, components: components).commit(on_conflict: 'keep') + expect(external.external_assessment_grades.find_by(course_user: alice).grade).to eq(50) + end + + it 'detects conflicts across multiple components' do + comps = [{ name: 'Midterm', weightage: 30, maximum_grade: 50 }, + { name: 'Final', weightage: 50, maximum_grade: 100 }] + # Seed both components + seed_csv = "Identifier,Midterm,Final\nA001,10,80\n" + service(csv_data: seed_csv, components: comps).commit(on_conflict: 'replace') + # Re-import with different values + csv = "Identifier,Midterm,Final\nA001,20,90\n" + result = service(csv_data: csv, components: comps).preview + expect(result[:conflicts].map { |c| c[:component] }).to contain_exactly('Midterm', 'Final') + end + end + + describe 'determinacy' do + let(:components) { [name: 'Midterm', weightage: 30, maximum_grade: 50] } + + it 'does not move a grade when the student external_id changes after import' do + csv = "Identifier,Midterm\nA001,41\n" + service(csv_data: csv, components: components).commit(on_conflict: 'replace') + grade = Course::ExternalAssessmentGrade.last + alice.update!(external_id: 'CHANGED') + expect(grade.reload.course_user_id).to eq(alice.id) + expect(grade.grade).to eq(41) + end + end + end +end