This commit is contained in:
NanGua-QWQ
2026-04-11 13:33:08 +08:00
29 changed files with 8627 additions and 149 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -23,6 +23,7 @@
"@tauri-apps/plugin-shell": "^2.3.5",
"antd": "^6.3.1",
"appwrite": "^24.0.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"i18next": "^25.8.14",
"json-rules-engine": "^7.3.1",
@@ -36,6 +37,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2.5.0",
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.19.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
+16
View File
@@ -26,6 +26,9 @@ importers:
appwrite:
specifier: ^24.0.0
version: 24.0.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dayjs:
specifier: ^1.11.20
version: 1.11.20
@@ -60,6 +63,9 @@ importers:
'@tauri-apps/cli':
specifier: ^2.5.0
version: 2.10.1
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/node':
specifier: ^22.19.1
version: 22.19.15
@@ -1043,6 +1049,9 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1302,6 +1311,9 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -3346,6 +3358,8 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@types/crypto-js@4.2.2': {}
'@types/estree@1.0.8': {}
'@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14)':
@@ -3705,6 +3719,8 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
crypto-js@4.2.0: {}
csstype@3.2.3: {}
data-view-buffer@1.0.2:
+19 -5
View File
@@ -360,13 +360,15 @@ pub async fn oauth_exchange_code(
platform_id: String,
platform_secret: String,
callback_url: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OAuthTokenResponse>, String> {
println!(
"[OAuth] 换取令牌 - code: {}, platform_id: {}, callback_url: {}",
code, platform_id, callback_url
);
let client = reqwest::Client::new();
let state_guard = state.read();
let client = &state_guard.http_client;
let params = [
("grant_type", "authorization_code"),
("code", &code),
@@ -408,8 +410,11 @@ pub async fn oauth_revoke_token(
token_type_hint: Option<String>,
platform_id: String,
platform_secret: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<()>, String> {
let client = reqwest::Client::new();
let state_guard = state.read();
let client = &state_guard.http_client;
let mut payload = serde_json::json!({
"token": token,
"client_id": platform_id,
@@ -443,8 +448,11 @@ pub async fn oauth_introspect_token(
token: String,
platform_id: String,
platform_secret: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OAuthIntrospectResponse>, String> {
let client = reqwest::Client::new();
let state_guard = state.read();
let client = &state_guard.http_client;
let response = client
.post("https://sectl.top/api/oauth/introspect")
.json(&serde_json::json!({
@@ -475,8 +483,11 @@ pub async fn oauth_introspect_token(
#[tauri::command]
pub async fn oauth_get_user_info(
access_token: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OAuthUserInfo>, String> {
let client = reqwest::Client::new();
let state_guard = state.read();
let client = &state_guard.http_client;
let response = client
.get("https://sectl.top/api/oauth/userinfo")
.header("Authorization", format!("Bearer {}", access_token))
@@ -505,8 +516,11 @@ pub async fn oauth_refresh_token(
refresh_token: String,
platform_id: String,
platform_secret: String,
state: State<'_, Arc<RwLock<AppState>>>,
) -> Result<IpcResponse<OAuthTokenResponse>, String> {
let client = reqwest::Client::new();
let state_guard = state.read();
let client = &state_guard.http_client;
let response = client
.post("https://sectl.top/api/oauth/token")
.json(&serde_json::json!({
+2 -2
View File
@@ -2,10 +2,10 @@ use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::BTreeSet;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::process::Command;
use std::sync::Arc;
use tauri::{AppHandle, Emitter, State};
#[cfg(target_os = "windows")]
use winreg::enums::HKEY_LOCAL_MACHINE;
#[cfg(target_os = "windows")]
+178 -139
View File
@@ -1,7 +1,7 @@
use chrono::{DateTime, Months, Utc};
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
QueryFilter, Set, Statement, TransactionTrait,
ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter,
Set, Statement, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
@@ -523,7 +523,9 @@ fn normalize_execution_config(
Ok(config)
}
fn normalize_filter_config(mut config: AutoScoreFilterConfig) -> Result<AutoScoreFilterConfig, String> {
fn normalize_filter_config(
mut config: AutoScoreFilterConfig,
) -> Result<AutoScoreFilterConfig, String> {
config.groups = dedupe_trimmed_strings(config.groups);
config.grades = dedupe_trimmed_strings(config.grades);
if let (Some(min_score), Some(max_score)) = (config.min_score, config.max_score) {
@@ -539,7 +541,10 @@ fn normalize_filter_config(mut config: AutoScoreFilterConfig) -> Result<AutoScor
fn build_trigger_tree_from_triggers(triggers: &[AutoScoreTrigger]) -> JsonValue {
JsonValue::Object(
[
("id".to_string(), JsonValue::String(Uuid::new_v4().to_string())),
(
"id".to_string(),
JsonValue::String(Uuid::new_v4().to_string()),
),
("type".to_string(), JsonValue::String("group".to_string())),
(
"properties".to_string(),
@@ -614,7 +619,10 @@ fn build_trigger_rule_node(trigger: &AutoScoreTrigger) -> JsonValue {
JsonValue::Object(
[
("id".to_string(), JsonValue::String(Uuid::new_v4().to_string())),
(
"id".to_string(),
JsonValue::String(Uuid::new_v4().to_string()),
),
("type".to_string(), JsonValue::String("rule".to_string())),
(
"properties".to_string(),
@@ -707,7 +715,10 @@ fn normalize_trigger_tree_group(node: JsonValue) -> Result<JsonValue, String> {
"not": not,
}),
),
("children1".to_string(), JsonValue::Array(normalized_children)),
(
"children1".to_string(),
JsonValue::Array(normalized_children),
),
]
.into_iter()
.collect(),
@@ -843,7 +854,11 @@ fn trigger_from_rule_node(node: &JsonValue) -> Result<AutoScoreTrigger, String>
};
let trigger = AutoScoreTrigger {
event: event.to_string(),
value: if score.trim().is_empty() { None } else { Some(score) },
value: if score.trim().is_empty() {
None
} else {
Some(score)
},
};
normalize_trigger(trigger)
}
@@ -1033,7 +1048,11 @@ fn parse_positive_i64(value: &JsonValue) -> Option<i64> {
fn parse_non_negative_i64(value: &JsonValue) -> Option<i64> {
match value {
JsonValue::Number(number) => number.as_i64().filter(|parsed| *parsed >= 0),
JsonValue::String(text) => text.trim().parse::<i64>().ok().filter(|parsed| *parsed >= 0),
JsonValue::String(text) => text
.trim()
.parse::<i64>()
.ok()
.filter(|parsed| *parsed >= 0),
_ => None,
}
}
@@ -1090,12 +1109,10 @@ fn stringify_interval_trigger_value(value: &IntervalTriggerValue) -> Option<Stri
match unit {
IntervalUnit::Minute => Some(amount.to_string()),
IntervalUnit::Day | IntervalUnit::Month => serde_json::to_string(
&json!({
"amount": amount,
"unit": unit,
}),
)
IntervalUnit::Day | IntervalUnit::Month => serde_json::to_string(&json!({
"amount": amount,
"unit": unit,
}))
.ok(),
}
}
@@ -1127,9 +1144,7 @@ fn add_interval_to_time(
IntervalUnit::Minute => {
base_time.checked_add_signed(chrono::Duration::minutes(*amount))
}
IntervalUnit::Day => {
base_time.checked_add_signed(chrono::Duration::days(*amount))
}
IntervalUnit::Day => base_time.checked_add_signed(chrono::Duration::days(*amount)),
IntervalUnit::Month => {
let months = u32::try_from(*amount).ok()?;
base_time.checked_add_months(Months::new(months))
@@ -1349,7 +1364,9 @@ fn deserialize_batches(value: &JsonValue) -> Vec<AutoScoreExecutionBatch> {
.collect()
}
async fn load_batches_from_settings(state: &SafeAppState) -> Result<Vec<AutoScoreExecutionBatch>, String> {
async fn load_batches_from_settings(
state: &SafeAppState,
) -> Result<Vec<AutoScoreExecutionBatch>, String> {
let state_guard = state.read();
let db_conn = state_guard.db.read().clone();
let mut settings = state_guard.settings.write();
@@ -1378,7 +1395,9 @@ async fn save_batches_to_settings(
Ok(())
}
pub async fn query_execution_batches(state: &SafeAppState) -> Result<Vec<AutoScoreExecutionBatch>, String> {
pub async fn query_execution_batches(
state: &SafeAppState,
) -> Result<Vec<AutoScoreExecutionBatch>, String> {
let mut batches = load_batches_from_settings(state).await?;
batches.sort_by(|a, b| b.run_at.cmp(&a.run_at));
Ok(batches)
@@ -1679,15 +1698,11 @@ async fn execute_rule(
.iter()
.any(|action| matches!(action, PlannedAction::SettleScore));
let mut daily_score_delta_used: i64 = if mode == ExecutionMode::Normal {
execution_batches
.iter()
.filter(|batch| batch.rule_id == rule.id && !batch.rolled_back && is_same_utc_day(&batch.run_at))
.map(|batch| batch.score_delta_total.abs())
.sum()
} else {
0
};
let mut daily_score_delta_used: i64 = execution_batches
.iter()
.filter(|batch| batch.rule_id == rule.id && !batch.rolled_back && is_same_utc_day(&batch.run_at))
.map(|batch| batch.score_delta_total.abs())
.sum();
let txn = conn.begin().await.map_err(|e| e.to_string())?;
let mut stats = RuleExecutionStats::default();
@@ -1799,9 +1814,9 @@ async fn execute_rule(
}
fn plan_actions(actions: &[AutoScoreAction]) -> Result<Vec<PlannedAction>, String> {
let mut planned = Vec::new();
for action in actions {
match action.event.as_str() {
let mut planned = Vec::new();
for action in actions {
match action.event.as_str() {
"add_score" => {
let delta = action
.value
@@ -1822,57 +1837,62 @@ fn plan_actions(actions: &[AutoScoreAction]) -> Result<Vec<PlannedAction>, Strin
planned.push(PlannedAction::SettleScore);
}
_ => {}
}
}
}
Ok(planned)
Ok(planned)
}
async fn execute_settlement(conn: &DatabaseConnection) -> Result<(), String> {
let backend = conn.get_database_backend();
let count_stmt = Statement::from_string(
backend,
"SELECT COUNT(*) AS cnt FROM score_events WHERE settlement_id IS NULL",
);
let count_row = conn.query_one(count_stmt).await.map_err(|e| e.to_string())?;
let Some(count_row) = count_row else {
return Ok(());
};
let unsettled_count = try_get_i64(&count_row, "cnt").unwrap_or(0);
if unsettled_count <= 0 {
return Ok(());
}
let last_end_sql = match backend {
sea_orm::DatabaseBackend::Sqlite => {
"SELECT end_time AS end_time FROM settlements ORDER BY julianday(end_time) DESC LIMIT 1"
let backend = conn.get_database_backend();
let count_stmt = Statement::from_string(
backend,
"SELECT COUNT(*) AS cnt FROM score_events WHERE settlement_id IS NULL",
);
let count_row = conn
.query_one(count_stmt)
.await
.map_err(|e| e.to_string())?;
let Some(count_row) = count_row else {
return Ok(());
};
let unsettled_count = try_get_i64(&count_row, "cnt").unwrap_or(0);
if unsettled_count <= 0 {
return Ok(());
}
sea_orm::DatabaseBackend::Postgres => {
"SELECT end_time AS end_time FROM settlements ORDER BY end_time DESC LIMIT 1"
}
_ => "SELECT end_time AS end_time FROM settlements ORDER BY end_time DESC LIMIT 1",
};
let min_event_sql =
"SELECT MIN(event_time) AS min_event_time FROM score_events WHERE settlement_id IS NULL";
let last_end = conn
.query_one(Statement::from_string(backend, last_end_sql))
.await
.map_err(|e| e.to_string())?
.and_then(|row| try_get_string(&row, "end_time"));
let last_end_sql = match backend {
sea_orm::DatabaseBackend::Sqlite => {
"SELECT end_time AS end_time FROM settlements ORDER BY julianday(end_time) DESC LIMIT 1"
}
sea_orm::DatabaseBackend::Postgres => {
"SELECT end_time AS end_time FROM settlements ORDER BY end_time DESC LIMIT 1"
}
_ => "SELECT end_time AS end_time FROM settlements ORDER BY end_time DESC LIMIT 1",
};
let min_event_sql =
"SELECT MIN(event_time) AS min_event_time FROM score_events WHERE settlement_id IS NULL";
let min_event_time = conn
.query_one(Statement::from_string(backend, min_event_sql))
.await
.map_err(|e| e.to_string())?
.and_then(|row| try_get_string(&row, "min_event_time"));
let last_end = conn
.query_one(Statement::from_string(backend, last_end_sql))
.await
.map_err(|e| e.to_string())?
.and_then(|row| try_get_string(&row, "end_time"));
let end_time = now_iso();
let start_time = last_end.or(min_event_time).unwrap_or_else(|| end_time.clone());
let created_at = end_time.clone();
let min_event_time = conn
.query_one(Statement::from_string(backend, min_event_sql))
.await
.map_err(|e| e.to_string())?
.and_then(|row| try_get_string(&row, "min_event_time"));
match backend {
sea_orm::DatabaseBackend::Postgres => {
let insert_stmt = Statement::from_string(
let end_time = now_iso();
let start_time = last_end
.or(min_event_time)
.unwrap_or_else(|| end_time.clone());
let created_at = end_time.clone();
match backend {
sea_orm::DatabaseBackend::Postgres => {
let insert_stmt = Statement::from_string(
backend,
format!(
"INSERT INTO settlements (start_time, end_time, created_at) VALUES ('{}', '{}', '{}') RETURNING id",
@@ -1881,74 +1901,82 @@ async fn execute_settlement(conn: &DatabaseConnection) -> Result<(), String> {
created_at.replace('\'', "''")
),
);
let row = conn
.query_one(insert_stmt)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "Failed to create settlement".to_string())?;
let settlement_id = try_get_i32(&row, "id")
.ok_or_else(|| "Failed to read settlement id".to_string())?;
let row = conn
.query_one(insert_stmt)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "Failed to create settlement".to_string())?;
let settlement_id = try_get_i32(&row, "id")
.ok_or_else(|| "Failed to read settlement id".to_string())?;
let update_events_stmt = Statement::from_string(
backend,
format!(
"UPDATE score_events SET settlement_id = {} WHERE settlement_id IS NULL",
settlement_id
),
);
conn.execute(update_events_stmt).await.map_err(|e| e.to_string())?;
let update_events_stmt = Statement::from_string(
backend,
format!(
"UPDATE score_events SET settlement_id = {} WHERE settlement_id IS NULL",
settlement_id
),
);
conn.execute(update_events_stmt)
.await
.map_err(|e| e.to_string())?;
let update_students_stmt = Statement::from_string(
backend,
format!(
"UPDATE students SET score = 0, updated_at = '{}' ",
end_time.replace('\'', "''")
),
);
conn.execute(update_students_stmt).await.map_err(|e| e.to_string())?;
}
_ => {
let insert_stmt = Statement::from_string(
backend,
format!(
let update_students_stmt = Statement::from_string(
backend,
format!(
"UPDATE students SET score = 0, updated_at = '{}' ",
end_time.replace('\'', "''")
),
);
conn.execute(update_students_stmt)
.await
.map_err(|e| e.to_string())?;
}
_ => {
let insert_stmt = Statement::from_string(
backend,
format!(
"INSERT INTO settlements (start_time, end_time, created_at) VALUES ('{}', '{}', '{}')",
start_time.replace('\'', "''"),
end_time.replace('\'', "''"),
created_at.replace('\'', "''")
),
);
conn.execute(insert_stmt).await.map_err(|e| e.to_string())?;
);
conn.execute(insert_stmt).await.map_err(|e| e.to_string())?;
let id_stmt = Statement::from_string(backend, "SELECT last_insert_rowid() AS id");
let row = conn
.query_one(id_stmt)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "Failed to read settlement id".to_string())?;
let settlement_id = try_get_i32(&row, "id")
.ok_or_else(|| "Failed to read settlement id".to_string())?;
let id_stmt = Statement::from_string(backend, "SELECT last_insert_rowid() AS id");
let row = conn
.query_one(id_stmt)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| "Failed to read settlement id".to_string())?;
let settlement_id = try_get_i32(&row, "id")
.ok_or_else(|| "Failed to read settlement id".to_string())?;
let update_events_stmt = Statement::from_string(
backend,
format!(
"UPDATE score_events SET settlement_id = {} WHERE settlement_id IS NULL",
settlement_id
),
);
conn.execute(update_events_stmt).await.map_err(|e| e.to_string())?;
let update_events_stmt = Statement::from_string(
backend,
format!(
"UPDATE score_events SET settlement_id = {} WHERE settlement_id IS NULL",
settlement_id
),
);
conn.execute(update_events_stmt)
.await
.map_err(|e| e.to_string())?;
let update_students_stmt = Statement::from_string(
backend,
format!(
"UPDATE students SET score = 0, updated_at = '{}'",
end_time.replace('\'', "''")
),
);
conn.execute(update_students_stmt).await.map_err(|e| e.to_string())?;
let update_students_stmt = Statement::from_string(
backend,
format!(
"UPDATE students SET score = 0, updated_at = '{}'",
end_time.replace('\'', "''")
),
);
conn.execute(update_students_stmt)
.await
.map_err(|e| e.to_string())?;
}
}
}
Ok(())
Ok(())
}
#[derive(Debug, Default)]
@@ -1957,7 +1985,10 @@ struct TriggerEvalContext {
sql_refs_by_query: HashMap<String, StudentRefs>,
}
fn collect_sql_queries_from_tree(node: &JsonValue, queries: &mut Vec<String>) -> Result<(), String> {
fn collect_sql_queries_from_tree(
node: &JsonValue,
queries: &mut Vec<String>,
) -> Result<(), String> {
let Some(node_type) = node.get("type").and_then(JsonValue::as_str) else {
return Err("Trigger tree node type is required".to_string());
};
@@ -1979,7 +2010,12 @@ fn collect_sql_queries_from_tree(node: &JsonValue, queries: &mut Vec<String>) ->
trigger.event.as_str(),
"query_sql" | "student_query_sql" | "student_sql"
) {
if let Some(sql) = trigger.value.as_deref().map(str::trim).filter(|value| !value.is_empty()) {
if let Some(sql) = trigger
.value
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
queries.push(sql.to_string());
}
}
@@ -2094,7 +2130,9 @@ fn evaluate_trigger_tree_for_student(
.to_string();
ctx.sql_refs_by_query
.get(&sql)
.map(|refs| refs.ids.contains(&student.id) || refs.names.contains(&student.name))
.map(|refs| {
refs.ids.contains(&student.id) || refs.names.contains(&student.name)
})
.unwrap_or(false)
}
"student_score_gt" => trigger
@@ -2118,8 +2156,8 @@ fn evaluate_trigger_tree_for_student(
}
async fn resolve_target_students(
conn: &DatabaseConnection,
rule: &AutoScoreRule,
conn: &DatabaseConnection,
rule: &AutoScoreRule,
) -> Result<Vec<students::Model>, String> {
let all_students = students::Entity::find()
.all(conn)
@@ -2157,7 +2195,6 @@ async fn resolve_target_students(
Ok(matched)
}
async fn is_student_pass_cooldown(
conn: &DatabaseConnection,
execution_batches: &[AutoScoreExecutionBatch],
@@ -2181,16 +2218,18 @@ async fn is_student_pass_cooldown(
let run_at = DateTime::parse_from_rfc3339(batch.run_at.as_str())
.map(|value| value.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now() - chrono::Duration::days(3650));
if run_at >= cutoff && batch.affected_student_names.iter().any(|name| name == student_name)
if run_at >= cutoff
&& batch
.affected_student_names
.iter()
.any(|name| name == student_name)
{
return Ok(false);
}
}
let prefix = format!("{}#{}:", AUTO_SCORE_REASON_PREFIX, rule.id);
let since = cutoff
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
.to_string();
let since = cutoff.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
let exists = score_events::Entity::find()
.filter(score_events::Column::StudentName.eq(student_name.to_string()))
+8
View File
@@ -1,4 +1,5 @@
use parking_lot::RwLock;
use reqwest::Client;
use sea_orm::DatabaseConnection;
use std::sync::Arc;
use tauri::AppHandle;
@@ -19,6 +20,7 @@ pub struct AppState {
pub auto_score: Arc<RwLock<AutoScoreService>>,
pub logger: Arc<RwLock<LoggerService>>,
pub data: Arc<RwLock<DataService>>,
pub http_client: Client,
pub app_handle: AppHandle,
}
@@ -34,6 +36,11 @@ impl AppState {
let data = Arc::new(RwLock::new(DataService::new()));
let db = Arc::new(RwLock::new(None));
let http_client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self {
db,
settings,
@@ -44,6 +51,7 @@ impl AppState {
auto_score,
logger,
data,
http_client,
app_handle,
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ export function OAuthLogin({ visible, onClose, onSuccess }: OAuthLoginProps) {
if (result.code) {
console.log("[OAuth] 授权码:", result.code)
console.log("[OAuth] State:", result.state, "期望:", getExpectedState())
// 验证 state 防止 CSRF
const expectedState = getExpectedState()
if (expectedState && result.state !== expectedState) {
+285
View File
@@ -0,0 +1,285 @@
/**
* SECTL
*
*/
import React, { useState, useEffect } from "react"
import { Card, Button, Space, Alert, Descriptions, message, Modal, Typography, Divider } from "antd"
import {
CloudSyncOutlined,
UploadOutlined,
DownloadOutlined,
DeleteOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons"
import { scoreSyncService } from "../services/scoreSyncService"
import { useSectl } from "../contexts/SectlContext"
const { Title, Text } = Typography
interface SyncPanelProps {
// 从父组件获取数据的方法
onGetLocalData?: () => {
students: any[]
events: any[]
settings: { reasons: any[]; tags: any[] }
}
// 从云端恢复数据到本地的方法
onRestoreData?: (data: {
students: any[]
events: any[]
settings: { reasons: any[]; tags: any[] }
}) => void
}
export const ScoreSyncPanel: React.FC<SyncPanelProps> = ({ onGetLocalData, onRestoreData }) => {
const { isAuthenticated } = useSectl()
const [syncing, setSyncing] = useState(false)
const [restoring, setRestoring] = useState(false)
const [clearing, setClearing] = useState(false)
const [loading, setLoading] = useState(false)
const [metadata, setMetadata] = useState<any>(null)
const [hasData, setHasData] = useState(false)
// 检查云端数据状态
const checkCloudStatus = async () => {
if (!isAuthenticated) return
try {
setLoading(true)
const [meta, hasCloudData] = await Promise.all([
scoreSyncService.getSyncMetadata(),
scoreSyncService.hasCloudData(),
])
setMetadata(meta)
setHasData(hasCloudData)
} catch (error: any) {
console.error("检查云端状态失败:", error)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (isAuthenticated) {
checkCloudStatus()
}
}, [isAuthenticated])
// 上传数据到云端
const handleSyncToCloud = async () => {
if (!onGetLocalData) {
message.error("未配置数据获取方法")
return
}
try {
setSyncing(true)
const localData = onGetLocalData()
await scoreSyncService.fullSync(localData)
await checkCloudStatus()
message.success("数据已同步到云端")
} catch (error: any) {
message.error(`同步失败:${error.message}`)
} finally {
setSyncing(false)
}
}
// 从云端下载数据
const handleRestoreFromCloud = async () => {
Modal.confirm({
title: "确认恢复云端数据",
icon: <ExclamationCircleOutlined />,
content: "此操作将用云端数据覆盖本地数据,确定继续吗?",
okText: "确定",
cancelText: "取消",
onOk: async () => {
if (!onRestoreData) {
message.error("未配置数据恢复方法")
return
}
try {
setRestoring(true)
const cloudData = await scoreSyncService.restoreFromCloud()
onRestoreData({
students: cloudData.students as any[],
events: cloudData.events as any[],
settings: cloudData.settings,
})
message.success("数据已从云端恢复")
} catch (error: any) {
message.error(`恢复失败:${error.message}`)
} finally {
setRestoring(false)
}
},
})
}
// 清除云端数据
const handleClearCloudData = async () => {
Modal.confirm({
title: "确认清除云端数据",
icon: <ExclamationCircleOutlined />,
content: "此操作将永久删除云端的所有数据,且不可恢复!确定继续吗?",
okText: "确定",
cancelText: "取消",
okButtonProps: { danger: true },
onOk: async () => {
try {
setClearing(true)
await scoreSyncService.clearCloudData()
await checkCloudStatus()
message.success("云端数据已清除")
} catch (error: any) {
message.error(`清除失败:${error.message}`)
} finally {
setClearing(false)
}
},
})
}
if (!isAuthenticated) {
return (
<Card>
<Alert
message="请先登录 SECTL"
description="登录后可使用云端数据同步功能"
type="info"
showIcon
/>
</Card>
)
}
return (
<div>
<Card
title={
<Space>
<CloudSyncOutlined />
<span></span>
</Space>
}
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
{/* 云端状态 */}
<Alert
message={
hasData ? (
<Space>
<CheckCircleOutlined style={{ color: "#52c41a" }} />
<span></span>
</Space>
) : (
<Space>
<ExclamationCircleOutlined style={{ color: "#faad14" }} />
<span></span>
</Space>
)
}
description={
metadata ? (
<div>
<Text>
{new Date(metadata.lastSyncTime).toLocaleString("zh-CN")}
</Text>
<br />
<Text>{metadata.version}</Text>
</div>
) : (
"暂无同步记录"
)
}
type={hasData ? "success" : "warning"}
showIcon
/>
<Divider />
{/* 同步操作 */}
<div>
<Title level={5}></Title>
<Space direction="vertical" style={{ width: "100%" }}>
<Alert
message="数据同步说明"
description={
<div>
<ul style={{ margin: 0, paddingLeft: 20 }}>
<li></li>
<li></li>
<li></li>
</ul>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Space wrap>
<Button
type="primary"
icon={<UploadOutlined />}
onClick={handleSyncToCloud}
loading={syncing}
disabled={!onGetLocalData}
>
</Button>
<Button
icon={<DownloadOutlined />}
onClick={handleRestoreFromCloud}
loading={restoring}
disabled={!hasData || !onRestoreData}
>
</Button>
<Button
danger
icon={<DeleteOutlined />}
onClick={handleClearCloudData}
loading={clearing}
disabled={!hasData}
>
</Button>
<Button icon={<CloudSyncOutlined />} onClick={checkCloudStatus} loading={loading}>
</Button>
</Space>
</Space>
</div>
<Divider />
{/* 同步记录 */}
<div>
<Title level={5}></Title>
{metadata ? (
<Descriptions bordered column={1} size="small">
<Descriptions.Item label="最后同步时间">
{new Date(metadata.lastSyncTime).toLocaleString("zh-CN")}
</Descriptions.Item>
<Descriptions.Item label="数据版本">{metadata.version}</Descriptions.Item>
<Descriptions.Item label="平台">
{metadata.platform || "SecScore"}
</Descriptions.Item>
</Descriptions>
) : (
<Text type="secondary"></Text>
)}
</div>
</Space>
</Card>
</div>
)
}
+313
View File
@@ -0,0 +1,313 @@
/**
* SECTL
*/
import React, { useState, useEffect } from "react"
import {
Card,
Table,
Button,
Upload,
Space,
Progress,
Modal,
Input,
message,
Popconfirm,
Typography,
} from "antd"
import {
CloudOutlined,
UploadOutlined,
DownloadOutlined,
DeleteOutlined,
ShareAltOutlined,
ReloadOutlined,
FileOutlined,
} from "@ant-design/icons"
import type { UploadFile } from "antd"
import { sectlCloudStorage, CloudFile, StorageUsage } from "../services/sectlCloudStorage"
import { useSectl } from "../contexts/SectlContext"
const { Title } = Typography
export const SectlCloudStorageManager: React.FC = () => {
const { isAuthenticated } = useSectl()
const [files, setFiles] = useState<CloudFile[]>([])
const [loading, setLoading] = useState(false)
const [storageUsage, setStorageUsage] = useState<StorageUsage | null>(null)
const [uploading, setUploading] = useState(false)
const [renameModal, setRenameModal] = useState<{
visible: boolean
fileId?: string
filename?: string
}>({ visible: false })
const [newFilename, setNewFilename] = useState("")
// 加载文件列表
const loadFiles = async () => {
if (!isAuthenticated) return
try {
setLoading(true)
const result = await sectlCloudStorage.listFiles({ limit: 100 })
setFiles(result.files)
} catch (error: any) {
message.error(`加载文件列表失败:${error.message}`)
} finally {
setLoading(false)
}
}
// 加载存储使用情况
const loadStorageUsage = async () => {
if (!isAuthenticated) return
try {
const usage = await sectlCloudStorage.getStorageUsage()
setStorageUsage(usage)
} catch (error: any) {
console.error("加载存储使用情况失败:", error)
}
}
useEffect(() => {
if (isAuthenticated) {
loadFiles()
loadStorageUsage()
}
}, [isAuthenticated])
// 处理文件上传
const handleUpload = async (file: UploadFile): Promise<void> => {
if (!file.originFileObj) return
try {
setUploading(true)
await sectlCloudStorage.uploadFile(file.originFileObj)
message.success(`文件 ${file.name} 上传成功`)
loadFiles()
loadStorageUsage()
} catch (error: any) {
message.error(`上传失败:${error.message}`)
} finally {
setUploading(false)
}
}
// 处理文件下载
const handleDownload = async (file: CloudFile) => {
try {
const { download_url } = await sectlCloudStorage.downloadFile(file.file_id)
window.open(download_url, "_blank")
message.success("开始下载")
} catch (error: any) {
message.error(`下载失败:${error.message}`)
}
}
// 处理文件删除
const handleDelete = async (file: CloudFile) => {
try {
await sectlCloudStorage.deleteFile(file.file_id)
message.success("文件已删除")
loadFiles()
loadStorageUsage()
} catch (error: any) {
message.error(`删除失败:${error.message}`)
}
}
// 处理文件重命名
const handleRename = async () => {
if (!renameModal.fileId || !newFilename) return
try {
await sectlCloudStorage.renameFile(renameModal.fileId, newFilename)
message.success("重命名成功")
setRenameModal({ visible: false })
setNewFilename("")
loadFiles()
} catch (error: any) {
message.error(`重命名失败:${error.message}`)
}
}
// 创建分享链接
const handleCreateShare = async (file: CloudFile) => {
try {
const share = await sectlCloudStorage.createShare(file.file_id, {
expires_in: 86400, // 1 天
})
// 复制到剪贴板
await navigator.clipboard.writeText(share.share_url)
message.success("分享链接已创建并复制到剪贴板")
} catch (error: any) {
message.error(`创建分享失败:${error.message}`)
}
}
// 表格列定义
const columns = [
{
title: "文件名",
dataIndex: "filename",
key: "filename",
render: (text: string, record: CloudFile) => (
<Space>
<FileOutlined />
<span>{text}</span>
<Typography.Text type="secondary" style={{ fontSize: "12px" }}>
.{record.extension}
</Typography.Text>
</Space>
),
},
{
title: "大小",
dataIndex: "size_formatted",
key: "size_formatted",
width: 100,
},
{
title: "上传时间",
dataIndex: "created_at",
key: "created_at",
width: 180,
render: (text: string) => new Date(text).toLocaleString("zh-CN"),
},
{
title: "操作",
key: "action",
width: 250,
render: (_: any, record: CloudFile) => (
<Space>
<Button type="link" icon={<DownloadOutlined />} onClick={() => handleDownload(record)}>
</Button>
<Button type="link" icon={<ShareAltOutlined />} onClick={() => handleCreateShare(record)}>
</Button>
<Button
type="link"
onClick={() =>
setRenameModal({
visible: true,
fileId: record.file_id,
filename: record.filename,
})
}
>
</Button>
<Popconfirm
title="确定要删除这个文件吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
]
if (!isAuthenticated) {
return (
<Card>
<div style={{ textAlign: "center", padding: "40px" }}>
<CloudOutlined style={{ fontSize: 48, color: "#999" }} />
<Title level={4} style={{ marginTop: 16 }}>
SECTL 使
</Title>
</div>
</Card>
)
}
return (
<div>
<Card
title={
<Space>
<CloudOutlined />
<span></span>
</Space>
}
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={loadFiles} loading={loading}>
</Button>
<Upload
accept="*/*"
showUploadList={false}
beforeUpload={handleUpload}
disabled={uploading}
>
<Button type="primary" icon={<UploadOutlined />} loading={uploading}>
</Button>
</Upload>
</Space>
}
>
{/* 存储使用情况 */}
{storageUsage && (
<div style={{ marginBottom: 24 }}>
<Space direction="vertical" style={{ width: "100%" }} size="small">
<div>
{storageUsage.used_storage_formatted} / {storageUsage.total_storage_formatted}
</div>
<Progress
percent={storageUsage.percentage}
status={storageUsage.percentage > 90 ? "exception" : "active"}
/>
<div style={{ fontSize: "12px", color: "#999" }}>
{storageUsage.file_count}
</div>
</Space>
</div>
)}
{/* 文件列表 */}
<Table
columns={columns}
dataSource={files}
rowKey="file_id"
loading={loading}
pagination={{
pageSize: 20,
showSizeChanger: true,
showTotal: (total) => `${total} 个文件`,
}}
/>
</Card>
{/* 重命名对话框 */}
<Modal
title="重命名文件"
open={renameModal.visible}
onOk={handleRename}
onCancel={() => {
setRenameModal({ visible: false })
setNewFilename("")
}}
okText="确定"
cancelText="取消"
>
<Input
placeholder="请输入新文件名"
value={newFilename}
onChange={(e) => setNewFilename(e.target.value)}
onPressEnter={handleRename}
defaultValue={renameModal.filename}
/>
</Modal>
</div>
)
}
+67
View File
@@ -0,0 +1,67 @@
/**
* SECTL
*/
import React, { useState } from "react"
import { Button, message } from "antd"
import { LoginOutlined, UserOutlined } from "@ant-design/icons"
import { useSectl } from "../contexts/SectlContext"
interface SectlLoginButtonProps {
onLoginSuccess?: () => void
onLogoutSuccess?: () => void
scope?: string[]
}
export const SectlLoginButton: React.FC<SectlLoginButtonProps> = ({
onLoginSuccess,
onLogoutSuccess,
scope = ["user:read", "cloud:read", "cloud:write"],
}) => {
const { isAuthenticated, isLoading, userInfo, login, logout } = useSectl()
const [isLogging, setIsLogging] = useState(false)
const handleLogin = async () => {
try {
setIsLogging(true)
await login(scope)
message.success("登录成功!")
onLoginSuccess?.()
} catch (error: any) {
message.error(`登录失败:${error.message}`)
} finally {
setIsLogging(false)
}
}
const handleLogout = async () => {
try {
setIsLogging(true)
await logout()
message.success("已退出登录")
onLogoutSuccess?.()
} catch (error: any) {
message.error(`登出失败:${error.message}`)
} finally {
setIsLogging(false)
}
}
if (isLoading) {
return <Button loading>...</Button>
}
if (isAuthenticated) {
return (
<Button icon={<UserOutlined />} onClick={handleLogout} loading={isLogging} danger>
{userInfo?.name || "退出登录"}
</Button>
)
}
return (
<Button type="primary" icon={<LoginOutlined />} onClick={handleLogin} loading={isLogging}>
使 SECTL
</Button>
)
}
+157
View File
@@ -0,0 +1,157 @@
/**
* SECTL
*/
import React, { useState } from "react"
import { Card, Form, Input, Button, Space, Alert, Tag, Descriptions, message } from "antd"
import {
SettingOutlined,
UserOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
} from "@ant-design/icons"
import { useSectl } from "../contexts/SectlContext"
import { SectlLoginButton } from "./SectlLoginButton"
import { ScoreSyncPanel } from "./ScoreSyncPanel"
export const SectlSettingsPanel: React.FC = () => {
const { isAuthenticated, isLoading, userInfo, platformId, setPlatformId, refreshUserInfo } =
useSectl()
const [form] = Form.useForm()
const [isSaving, setIsSaving] = useState(false)
// 保存平台 ID 配置
const handleSaveConfig = async (values: any) => {
try {
setIsSaving(true)
setPlatformId(values.platformId)
// 保存到本地存储
localStorage.setItem("sectl_platform_id", values.platformId)
message.success("配置已保存")
} catch (error: any) {
message.error(`保存失败:${error.message}`)
} finally {
setIsSaving(false)
}
}
// 加载已保存的配置
React.useEffect(() => {
const savedPlatformId = localStorage.getItem("sectl_platform_id")
if (savedPlatformId) {
form.setFieldsValue({ platformId: savedPlatformId })
}
}, [form])
return (
<div>
{/* 认证状态卡片 */}
<Card
title={
<Space>
<UserOutlined />
<span>SECTL </span>
</Space>
}
style={{ marginBottom: 24 }}
>
<Space direction="vertical" style={{ width: "100%" }} size="large">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div>
<Space>
<span></span>
{isLoading ? (
<Tag color="processing">...</Tag>
) : isAuthenticated ? (
<Tag icon={<CheckCircleOutlined />} color="success">
</Tag>
) : (
<Tag icon={<CloseCircleOutlined />} color="default">
</Tag>
)}
</Space>
</div>
<SectlLoginButton onLoginSuccess={refreshUserInfo} onLogoutSuccess={() => {}} />
</div>
{isAuthenticated && userInfo && (
<Descriptions bordered column={2} size="small">
<Descriptions.Item label="用户 ID">{userInfo.user_id}</Descriptions.Item>
<Descriptions.Item label="用户名">{userInfo.name}</Descriptions.Item>
<Descriptions.Item label="邮箱">{userInfo.email}</Descriptions.Item>
<Descriptions.Item label="权限">{userInfo.permission}</Descriptions.Item>
<Descriptions.Item label="角色">{userInfo.role}</Descriptions.Item>
{userInfo.github_username && (
<Descriptions.Item label="GitHub">{userInfo.github_username}</Descriptions.Item>
)}
</Descriptions>
)}
</Space>
</Card>
{/* 平台配置卡片 */}
<Card
title={
<Space>
<SettingOutlined />
<span></span>
</Space>
}
style={{ marginBottom: 24 }}
>
<Form
form={form}
layout="vertical"
onFinish={handleSaveConfig}
initialValues={{ platformId }}
>
<Alert
message="平台 ID 配置"
description="请在 SECTL 控制台创建应用后填写平台 IDClient ID"
type="info"
showIcon
style={{ marginBottom: 24 }}
/>
<Form.Item
label="平台 ID (Client ID)"
name="platformId"
rules={[
{ required: true, message: "请输入平台 ID" },
{ pattern: /^pf_[a-zA-Z0-9]+$/, message: "平台 ID 格式不正确" },
]}
>
<Input placeholder="例如:pf_xxxxxxxxxxxx" />
</Form.Item>
<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
loading={isSaving}
icon={<SettingOutlined />}
>
</Button>
<Button
onClick={() => {
form.resetFields()
}}
>
</Button>
</Space>
</Form.Item>
</Form>
</Card>
{/* 数据同步面板 */}
<ScoreSyncPanel />
</div>
)
}
+147
View File
@@ -0,0 +1,147 @@
/**
* SECTL Context
* SECTL
*/
import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"
import { sectlAuth, UserInfo, TokenData } from "../services/sectlAuth"
interface SectlContextType {
isAuthenticated: boolean
isLoading: boolean
userInfo: UserInfo | null
token: TokenData | null
platformId: string
login: (scope?: string[]) => Promise<void>
logout: () => Promise<void>
setPlatformId: (platformId: string) => void
refreshUserInfo: () => Promise<void>
}
const SectlContext = createContext<SectlContextType | undefined>(undefined)
interface SectlProviderProps {
children: ReactNode
initialPlatformId?: string
}
export const SectlProvider: React.FC<SectlProviderProps> = ({
children,
initialPlatformId = "",
}) => {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [token, setToken] = useState<TokenData | null>(null)
const [platformId, setPlatformIdState] = useState(initialPlatformId)
// 初始化时检查是否有已保存的 Token
useEffect(() => {
const initAuth = async () => {
try {
if (platformId) {
sectlAuth.initialize(platformId)
}
const currentToken = sectlAuth.getToken()
if (currentToken) {
setToken(currentToken)
setIsAuthenticated(true)
// 验证 Token 是否有效
const isValid = await sectlAuth.introspectToken()
if (isValid.active) {
await loadUserInfo()
} else {
// Token 无效,尝试刷新
try {
await sectlAuth.refreshAccessToken()
const newToken = sectlAuth.getToken()
if (newToken) {
setToken(newToken)
await loadUserInfo()
} else {
setIsAuthenticated(false)
}
} catch {
setIsAuthenticated(false)
setToken(null)
}
}
}
} catch (error) {
console.error("初始化认证失败:", error)
setIsAuthenticated(false)
} finally {
setIsLoading(false)
}
}
initAuth()
}, [platformId])
const loadUserInfo = async () => {
try {
const info = await sectlAuth.getUserInfo()
setUserInfo(info)
} catch (error) {
console.error("加载用户信息失败:", error)
throw error
}
}
const login = async (scope: string[] = ["user:read"]) => {
try {
const tokenData = await sectlAuth.authorize(scope)
setToken(tokenData)
setIsAuthenticated(true)
await loadUserInfo()
} catch (error) {
console.error("登录失败:", error)
throw error
}
}
const logout = async () => {
try {
await sectlAuth.logout()
setToken(null)
setUserInfo(null)
setIsAuthenticated(false)
} catch (error) {
console.error("登出失败:", error)
throw error
}
}
const setPlatformId = (id: string) => {
setPlatformIdState(id)
sectlAuth.initialize(id)
}
const refreshUserInfo = async () => {
await loadUserInfo()
}
const value: SectlContextType = {
isAuthenticated,
isLoading,
userInfo,
token,
platformId,
login,
logout,
setPlatformId,
refreshUserInfo,
}
return <SectlContext.Provider value={value}>{children}</SectlContext.Provider>
}
export const useSectl = (): SectlContextType => {
const context = useContext(SectlContext)
if (context === undefined) {
throw new Error("useSectl 必须在 SectlProvider 内部使用")
}
return context
}
+43 -1
View File
@@ -2,23 +2,65 @@ import i18n from "i18next"
import { initReactI18next } from "react-i18next"
import zhCN from "./locales/zh-CN.json"
import enUS from "./locales/en-US.json"
import frFR from "./locales/fr-FR.json"
import esES from "./locales/es-ES.json"
import jaJP from "./locales/ja-JP.json"
import koKR from "./locales/ko-KR.json"
import ruRU from "./locales/ru-RU.json"
import deDE from "./locales/de-DE.json"
import ptBR from "./locales/pt-BR.json"
import arSA from "./locales/ar-SA.json"
export const defaultNS = "translation"
export const resources = {
"zh-CN": { translation: zhCN },
"en-US": { translation: enUS },
"fr-FR": { translation: frFR },
"es-ES": { translation: esES },
"ja-JP": { translation: jaJP },
"ko-KR": { translation: koKR },
"ru-RU": { translation: ruRU },
"de-DE": { translation: deDE },
"pt-BR": { translation: ptBR },
"ar-SA": { translation: arSA },
} as const
export type AppLanguage = "zh-CN" | "en-US"
export type AppLanguage =
| "zh-CN"
| "en-US"
| "fr-FR"
| "es-ES"
| "ja-JP"
| "ko-KR"
| "ru-RU"
| "de-DE"
| "pt-BR"
| "ar-SA"
export const languageNames: Record<AppLanguage, string> = {
"zh-CN": "简体中文",
"en-US": "English",
"fr-FR": "Français",
"es-ES": "Español",
"ja-JP": "日本語",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية",
}
export const languageOptions: { value: AppLanguage; label: string }[] = [
{ value: "zh-CN", label: "简体中文" },
{ value: "en-US", label: "English" },
{ value: "fr-FR", label: "Français" },
{ value: "es-ES", label: "Español" },
{ value: "ja-JP", label: "日本語" },
{ value: "ko-KR", label: "한국어" },
{ value: "ru-RU", label: "Русский" },
{ value: "de-DE", label: "Deutsch" },
{ value: "pt-BR", label: "Português" },
{ value: "ar-SA", label: "العربية" },
]
const savedLanguage = (() => {
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "التالي",
"prev": "السابق",
"finish": "إنهاء",
"cancel": "إلغاء",
"save": "حفظ",
"loading": "جاري التحميل...",
"confirm": "تأكيد",
"success": "نجاح",
"error": "خطأ",
"delete": "حذف",
"edit": "تعديل",
"add": "إضافة",
"search": "بحث",
"clear": "مسح",
"close": "إغلاق",
"yes": "نعم",
"no": "لا",
"submit": "إرسال",
"reset": "إعادة تعيين",
"import": "استيراد",
"export": "تصدير",
"create": "إنشاء",
"update": "تحديث",
"refresh": "تحديث",
"view": "عرض",
"name": "الاسم",
"status": "الحالة",
"operation": "العملية",
"description": "الوصف",
"total": "إجمالي {{count}} عناصر",
"noData": "لا توجد بيانات",
"pleaseSelect": "الرجاء الاختيار",
"pleaseEnter": "الرجاء الإدخال",
"readOnly": "وضع القراءة فقط",
"none": "لا يوجد",
"more": "المزيد"
},
"oobe": {
"title": "مرحبًا بك في SecScore",
"subtitle": "خبير إدارة النقاط التعليمية",
"step": "الخطوة {{current}}/{{total}}",
"skip": "تخطي",
"steps": {
"entry": {
"title": "البدء",
"description": "اختر الإجراء المطلوب تنفيذه",
"enterOobe": "دخول إلى OOBE",
"connectPostgresAutoSync": "الاتصال بـ PostgreSQL مع مزامنة تلقائية",
"skipDirect": "تخطي OOBE والدخول مباشرة"
},
"postgresql": {
"title": "الاتصال بـ PostgreSQL",
"description": "أدخل سلسلة اتصال PostgreSQL. سيقوم النظام بالاتصال والمزامنة تلقائيًا.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "الاتصال والمزامنة التلقائية"
},
"language": {
"title": "اختيار اللغة",
"description": "الرجاء اختيار لغتك المفضلة"
},
"theme": {
"title": "إعداد السمة",
"description": "اختر مظهر الواجهة",
"mode": "الوضع",
"lightMode": "فاتح",
"darkMode": "داكن",
"primaryColor": "اللون الأساسي",
"backgroundGradient": "تدرج الخلفية"
},
"password": {
"title": "إعداد كلمة المرور",
"description": "اضبط كلمة مرور لحماية بياناتك (اختياري)",
"adminPassword": "كلمة مرور المسؤول",
"adminPasswordHint": "الوصول إلى جميع الوظائف، 6 أرقام",
"pointsPassword": "كلمة مرور النقاط",
"pointsPasswordHint": "عمليات النقاط فقط، 6 أرقام",
"passwordPlaceholder": "أدخل 6 أرقام",
"hint": "اتركه فارغًا للتجاوز، قم بالإعداد لاحقًا في الإعدادات"
},
"students": {
"title": "استيراد قائمة الطلاب",
"description": "أضف قائمة طلاب لبدء إدارة النقاط",
"import": "استيراد القائمة",
"manual": "إضافة يدويًا",
"importHint": "يدعم ملفات Excel (.xlsx) أو JSON",
"manualHint": "الصق عدة أسطر، اسم طالب واحد لكل سطر",
"dragDrop": "اسحب وأفلت الملفات هنا أو انقر للتحديد",
"supportedFormats": "التنسيقات المدعومة: .xlsx, .json",
"studentName": "اسم الطالب",
"addStudent": "إضافة طالب",
"noStudents": "لا يوجد طلاب بعد، يرجى الإضافة أو الاستيراد",
"studentCount": "تمت إضافة {{count}} طالب",
"studentExists": "الطالب موجود بالفعل",
"importSuccess": "تم استيراد {{count}} طالب بنجاح",
"noNewStudents": "لا يوجد طلاب جدد للاستيراد",
"parseFailed": "فشل تحليل الملف",
"unsupportedFormat": "تنسيق الملف غير مدعوم"
},
"reasons": {
"title": "إعداد الأسباب",
"description": "أضف أسباب التغيير الشائعة للنقاط",
"addReason": "إضافة سبب",
"reasonName": "اسم السبب",
"points": "النقاط",
"positive": "إضافة نقاط",
"negative": "خصم نقاط",
"noReasons": "لا يوجد أسباب بعد، يرجى إضافة أسباب شائعة",
"reasonCount": "تمت إضافة {{count}} أسباب",
"reasonExists": "السبب موجود بالفعل"
},
"start": {
"title": "جاهز للاستخدام",
"description": "كل شيء جاهز. لنبدأ استخدام SecScore!",
"features": {
"score": "إدارة نقاط الطلاب بسهولة",
"history": "سجل تغييرات النقاط الكامل",
"settlement": "يدعم التسوية والأرشفة"
},
"startButton": "البدء"
}
}
},
"settings": {
"title": "إعدادات النظام",
"tabs": {
"appearance": "المظهر",
"security": "الأمان",
"account": "الحساب",
"database": "اتصال قاعدة البيانات",
"dataManagement": "إدارة البيانات",
"urlProtocol": "بروتوكول URL",
"about": "حول"
},
"language": "اللغة",
"languageHint": "اختر لغة عرض الواجهة",
"fontFamily": "الخط العام",
"fontFamilyHint": "اختر خط الواجهة (يتطلب تحديث)",
"theme": "السمة",
"password": "إعداد كلمة المرور",
"themeMode": "الوضع",
"lightMode": "فاتح",
"darkMode": "داكن",
"primaryColor": "اللون الأساسي",
"backgroundGradient": "تدرج الخلفية",
"searchKeyboard": {
"title": "لوحة المفاتيح للبحث",
"hint": "اختر تخطيط لوحة المفاتيح تحت حقل البحث",
"disableToggle": "تعطيل لوحة المفاتيح للبحث",
"disableHint": "لن تظهر لوحة المفاتيح تحت حقل البحث بعد الآن",
"options": {
"qwerty26": "26 مفتاح (QWERTY)",
"t9": "9 مفاتيح (T9)"
}
},
"interfaceZoom": "تكبير الواجهة",
"zoomOptions": {
"small70": "صغير (70%)",
"default100": "افتراضي (100%)",
"large150": "كبير (150%)"
},
"general": {
"saved": "تم الحفظ",
"saveFailed": "فشل الحفظ"
},
"zoomHint": "ضبط الحجم الكلي للواجهة",
"mobile": {
"navigationTitle": "مدخلات الصفحة",
"bottomNav": {
"label": "وظائف شريط التنقل السفلي",
"hint": "اختر الوظائف التي ستظهر في الشريط السفلي للهاتف المحمول. يعرض الشريط ما يصل إلى 4 عناصر، والباقي ينتقل إلى «المزيد»."
},
"dragHint": "اسحب لإعادة الترتيب. تظهر أول 4 عناصر في الشريط السفلي، والباقي في «المزيد».",
"dropHere": "أفلت هنا",
"bottomSection": "الشريط السفلي (الحد الأقصى 4)",
"moreSection": "المزيد",
"moveToMore": "نقل إلى المزيد",
"moveToBottom": "نقل للأسفل"
},
"zoomUpdated": "تم تحديث تكبير الواجهة",
"updateFailed": "فشل التحديث",
"security": {
"title": "نظام حماية كلمة المرور",
"adminPassword": "كلمة مرور المسؤول",
"pointsPassword": "كلمة مرور النقاط",
"recoveryString": "نص الاسترداد",
"set": "تم الإعداد",
"notSet": "غير مُعد",
"generated": "تم الإنشاء",
"notGenerated": "غير مُنشأ",
"enterPassword": "أدخل 6 أرقام (اتركه فارغًا للتجاوز)",
"adminPasswordPlaceholder": "أدخل 6 أرقام لكلمة مرور المسؤول (اتركه فارغًا للتجاوز)",
"pointsPasswordPlaceholder": "أدخل 6 أرقام لكلمة مرور النقاط (اتركه فارغًا للتجاوز)",
"recoveryPlaceholder": "أدخل نص الاسترداد",
"savePassword": "حفظ كلمة المرور",
"savePasswords": "حفظ كلمات المرور",
"generateRecovery": "إنشاء نص الاسترداد",
"clearAllPasswords": "مسح جميع كلمات المرور",
"recoveryReset": "إعادة تعيين نص الاسترداد",
"enterRecovery": "أدخل نص الاسترداد",
"resetPassword": "إعادة تعيين كلمة المرور",
"resetHint": "ستقوم إعادة التعيين بمسح كلمات مرور المسؤول والنقاط وستنشئ نص استرداد جديد.",
"confirmClear": "تأكيد مسح جميع كلمات المرور؟",
"clearHint": "بعد المسح، سيتم تعطيل الحماية (بدون كلمة مرور = إذن المسؤول افتراضيًا).",
"saved": "تم تحديث كلمة المرور",
"saveFailed": "فشل التحديث",
"generateFailed": "فشل الإنشاء",
"resetFailed": "فشل إعادة التعيين",
"cleared": "تم المسح",
"clearFailed": "فشل المسح",
"passwordClearedNewRecovery": "تم مسح كلمة المرور، نص استرداد جديد",
"newRecoveryString": "نص استرداد جديد (الرجاء الحفظ)"
},
"account": {
"title": "حساب SECTL Auth",
"notLoggedIn": "غير مسجل الدخول إلى SECTL Auth",
"oauthHint": "سجل الدخول بحساب SECTL Auth لاستخدام المصادقة الموحدة وتسجيل الخروج عن بُعد",
"loginButton": "تسجيل الدخول عبر SECTL Auth",
"loginSuccess": "تم تسجيل الدخول بنجاح",
"logout": "تسجيل الخروج",
"userId": "معرف المستخدم",
"permission": "مستوى الإذن"
},
"database": {
"title": "اتصال قاعدة البيانات",
"currentStatus": "حالة قاعدة البيانات الحالية",
"sqliteLocal": "قاعدة بيانات SQLite المحلية",
"postgresqlRemote": "قاعدة بيانات PostgreSQL البعيدة",
"connected": "متصل",
"disconnected": "غير متصل",
"postgresqlConnection": "اتصال PostgreSQL البعيد",
"connectionHint": "أدخل سلسلة اتصال PostgreSQL للاتصال بقاعدة بيانات بعيدة، تدعم المزامنة عبر منصات متعددة.",
"testConnection": "اختبار الاتصال",
"switchToPostgreSQL": "التبديل إلى PostgreSQL",
"switchToSQLite": "التبديل إلى SQLite المحلي",
"connectionTestSuccess": "اختبار الاتصال ناجح",
"connectionTestFailed": "فشل اختبار الاتصال",
"switchedTo": "تم التبديل إلى قاعدة بيانات {{type}}",
"switchedToSQLite": "تم التبديل إلى قاعدة بيانات SQLite المحلية",
"switchFailed": "فشل التبديل",
"syncDescription": "تعليمات المزامنة عبر منصات متعددة",
"syncPoint1": "يسمح استخدام قاعدة بيانات PostgreSQL البعيدة بمزامنة البيانات عبر منصات متعددة.",
"syncPoint2": "آليات قائمة العمليات المدمجة لضمان اتساق البيانات أثناء العمليات المتزامنة.",
"syncPoint3": "يلزم إعادة تشغيل التطبيق بعد تبديل قاعدة البيانات.",
"syncPoint4": "خدمات قواعد البيانات السحابية الموصى بها: Neon, Supabase, AWS RDS, إلخ.",
"enterConnectionString": "الرجاء إدخال سلسلة اتصال PostgreSQL",
"connectionExample": "مثال: postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "رفع البيانات المحلية إلى الخادم البعيد",
"uploadHint": "بعد الاتصال بقاعدة البيانات البعيدة، ارفع ودمج سجل SQLite المحلي إلى الخادم البعيد.",
"uploadNeedRemote": "الرجاء التبديل والاتصال بقاعدة بيانات PostgreSQL البعيدة أولاً",
"noNeedUpload": "البيانات المحلية والبعيدة متسقة بالفعل",
"uploadConfirmTitle": "تأكيد رفع البيانات المحلية إلى الخادم البعيد؟",
"uploadConfirmContent": "ست ưuوي المزامنة البيانات المحلية. تم الكشف عن: محلي فقط {{localOnly}}, بعيد فقط {{remoteOnly}}, تعارضات {{conflicts}}.",
"uploadSuccess": "تم رفع البيانات المحلية إلى الخادم البعيد",
"uploadFailed": "فشل الرفع"
},
"data": {
"title": "إدارة البيانات",
"settlement": "التسوية",
"settlementAndRestart": "التسوية وإعادة التشغيل",
"settlementHint": "تصنيف السجلات غير المسوّاة في مرحلة واحدة وإعادة تعيين جميع نقاط الطلاب إلى الصفر.",
"importExport": "استيراد / تصدير",
"exportJson": "تصدير JSON",
"importJson": "استيراد JSON",
"importHint": "سيؤدي الاستيراد إلى استبدال الطلاب/الأسباب/السجلات/الإعدادات الحالية (الإعدادات الأمنية مستثناة).",
"logs": "السجلات",
"logLevel": "مستوى السجل",
"logOperation": "عملية السجلات",
"viewLogs": "عرض السجلات",
"exportLogs": "تصدير السجلات",
"clearLogs": "مسح السجلات",
"logsCleared": "تم مسح السجلات",
"systemLogs": "سجلات النظام (آخر 200)",
"noLogs": "لا توجد سجلات",
"logLevelUpdated": "تم تحديث مستوى السجل",
"readLogsFailed": "فشل قراءة السجلات",
"logsExported": "تم تصدير السجلات",
"exportFailed": "فشل التصدير",
"exportSuccess": "تم التصدير بنجاح",
"importSuccess": "تم الاستيراد بنجاح، جاري التحديث",
"importFailed": "فشل الاستيراد",
"clearFailed": "فشل المسح",
"confirmSettlement": "تأكيد التسوية وإعادة التشغيل؟",
"settlementConfirm1": "سيؤدي هذا إلى أرشفة السجلات غير المسوّاة في مرحلة واحدة وإعادة تعيين جميع نقاط الطلاب إلى الصفر.",
"settlementConfirm2": "تبقى قائمة الطلاب دون تغيير؛ سجل التسوية متاح في صفحة «سجل التسوية».",
"settlementSuccess": "تمت التسوية بنجاح، تم إعادة تشغيل النقاط",
"settlementFailed": "فشل التسوية",
"settle": "التسوية",
"logLevels": {
"debug": "DEBUG (تصحيح)",
"info": "INFO (معلومات)",
"warn": "WARN (تحذير)",
"error": "ERROR (خطأ)"
}
},
"url": {
"title": "بروتوكول URL (secscore://)",
"protocol": "بروتوكول URL",
"description": "يمكنك استدعاء SecScore عبر URL لتنفيذ العمليات، على سبيل المثال:",
"register": "تسجيل بروتوكول URL",
"registered": "تم تسجيل بروتوكول URL",
"registerFailed": "فشل التسجيل",
"installerRequired": "يتطلب الإصدار المثبت من SecScore، قد لا يعمل في وضع التطوير."
},
"mcp": {
"title": "خدمة MCP",
"description": "إدارة خدمة MCP HTTP المدمجة لعملاء MCP الخارجيين.",
"running": "جاري التشغيل",
"stopped": "متوقف",
"noUrl": "لا يوجد URL",
"host": "المضيف",
"port": "المنفذ",
"start": "بدء",
"stop": "إيقاف",
"refresh": "تحديث الحالة",
"hint": "يوصى بالارتباط بـ 127.0.0.1 فقط؛ بعد البدء، نقطة النهاية هي http://host:port/mcp",
"hostRequired": "الرجاء إدخال مضيف MCP",
"portInvalid": "الرجاء إدخال منفذ صالح (1-65535)",
"startSuccess": "تم بدء خادم MCP",
"startFailed": "فشل بدء خادم MCP",
"stopSuccess": "تم إيقاف خادم MCP",
"stopFailed": "فشل إيقاف خادم MCP",
"startTimeout": "انتهى وقت بدء خادم MCP، يرجى المحاولة مرة أخرى",
"stopTimeout": "انتهى وقت إيقاف خادم MCP، يرجى المحاولة مرة أخرى"
},
"app": {
"title": "تحكمات التطبيق",
"description": "إعادة تشغيل أو إغلاق التطبيق بسرعة.",
"restart": "إعادة تشغيل التطبيق",
"quit": "إغلاق التطبيق",
"confirmRestartTitle": "تأكيد إعادة تشغيل التطبيق؟",
"confirmRestartContent": "سيتم إغلاق التطبيق وإعادة تشغيله فورًا.",
"confirmQuitTitle": "تأكيد إغلاق التطبيق؟",
"confirmQuitContent": "سيتم إغلاق التطبيق فورًا.",
"restartFailed": "فشل إعادة التشغيل",
"quitFailed": "فشل الإغلاق"
},
"about": {
"title": "حول",
"appName": "إدارة النقاط التعليمية",
"version": "الإصدار",
"copyright": "حقوق الطبع والنشر",
"ipcStatus": "حالة IPC",
"ipcConnected": "متصل",
"ipcDisconnected": "غير متصل (فشل التحميل المسبق)",
"environment": "البيئة",
"toggleDevTools": "تبديل أدوات المطور",
"license": "SecScore تحت ترخيص GPL3.0",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "الرئيسية",
"students": "إدارة الطلاب",
"score": "عمليات النقاط",
"boards": "اللوحات",
"leaderboard": "لوحة الصدارة",
"settlements": "سجل التسوية",
"reasons": "إدارة الأسباب",
"autoScore": "إضافة تلقائية للنقاط",
"rewardExchange": "تبادل المكافآت",
"rewardSettings": "إعدادات المكافآت",
"settings": "إعدادات النظام",
"remoteDb": "قاعدة البيانات البعيدة",
"refreshStatus": "تحديث الحالة",
"syncNow": "مزامنة الآن",
"syncSuccess": "تمت المزامنة بنجاح",
"syncFailed": "فشل المزامنة",
"getDbStatusFailed": "فشل في الحصول على حالة قاعدة البيانات",
"notRemoteMode": "وضع قاعدة البيانات البعيدة غير مفعل، يرجى إعادة تشغيل التطبيق",
"dbNotConnected": "قاعدة البيانات غير متصلة"
},
"auth": {
"unlock": "فتح الإذن",
"unlockHint": "أدخل كلمة المرور المكوّنة من 6 أرقام: كلمة مرور المسؤول = وصول كامل، كلمة مرور النقاط = عمليات النقاط فقط.",
"unlockButton": "فتح",
"unlocked": "تم فتح الإذن",
"logout": "تم التبديل إلى وضع القراءة فقط",
"passwordPlaceholder": "مثال: 123456",
"passwordError": "كلمة المرور غير صحيحة",
"enterPassword": "إدخال كلمة المرور",
"lock": "قفل"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "اللون والخلفية",
"gradientLabels": {
"blue": "أزرق منعش",
"pink": "وردي ناعم",
"cyan": "أزرق سماوي",
"purple": "بنفسجي"
},
"gradientDirections": {
"vertical": "عمودي",
"horizontal": "أفقي",
"diagonal": "قطري"
},
"generate": "إنشاء",
"saved": "تم الحفظ",
"saveFailed": "فشل الحفظ",
"mode": "الوضع",
"myTheme": "سمتي"
},
"permissions": {
"admin": "إذن المسؤول",
"points": "إذن النقاط",
"view": "قراءة فقط"
},
"home": {
"title": "الصفحة الرئيسية لنقاط الطلاب",
"subtitle": "إجمالي {{count}} طالب، انقر على البطاقة لتنفيذ العمليات",
"searchPlaceholder": "البحث بالاسم/البينين...",
"sortBy": {
"alphabet": "الترتيب حسب الاسم",
"surname": "المجموعة حسب الاسم العائلي",
"group": "الترتيب حسب المجموعة",
"score": "الترتيب حسب النقاط"
},
"layoutBy": {
"grouped": "قائمة مجمعة",
"squareGrid": "شبكة مربعة"
},
"noStudents": "لا توجد بيانات طلاب، يرجى الإضافة في إدارة الطلاب",
"noMatch": "لم يتم العثور على طلاب مطابقين",
"clearSearch": "مسح البحث",
"points": "نقاط",
"currentScore": "النقاط الحالية",
"quickOptions": "خيارات سريعة",
"adjustPoints": "ضبط النقاط",
"customPoints": "نقاط مخصصة",
"customPointsHint": "أدخل أي قيمة في حقل الإدخال",
"reason": "السبب",
"reasonPlaceholder": "أدخل سبب تغيير النقاط (اختياري)",
"preview": "معاينة التغيير",
"noReason": "(بدون سبب)",
"ungrouped": "غير مجمعة",
"category": {
"others": "أخرى"
},
"scoreAdded": "تمت إضافة {{points}} نقطة لـ {{name}}",
"scoreDeducted": "تم خصم {{points}} نقطة من {{name}}",
"submitFailed": "فشل الإرسال",
"pleaseSelectPoints": "الرجاء اختيار أو إدخال النقاط",
"reasonDefault": "{{action}} {{points}} نقطة",
"studentCount": "{{count}} طالب",
"operationTitle": "عملية النقاط: {{name}}",
"submitOperation": "إرسال العملية",
"operationTitleBatch": "عملية النقاط: تم اختيار {{count}}",
"undoLastAction": "تراجع",
"undoLastHint": "تراجع عن الأخير: {{name}} {{delta}}",
"undoUnavailable": "لا توجد عملية نقاط للتراجع عنها",
"undoLastSuccess": "تم التراجع عن آخر عملية نقاط",
"multiSelect": "اختيار متعدد",
"batchMode": "وضع الدفعة",
"selectAll": "تحديد الكل",
"clearSelected": "مسح التحديد",
"batchOperate": "نقاط الدفعة",
"selected": "تم التحديد",
"selectedCount": "تم اختيار {{count}}",
"batchSuccess": "تم إرسال النقاط لـ {{count}} طالب",
"batchPartial": "تم إرسال {{success}}/{{total}} طالب بنجاح",
"selectStudentFirst": "الرجاء تحديد طالب أو أكثر أولاً",
"addPoints": "إضافة نقاط",
"deductPoints": "خصم نقاط",
"pointsChange": "تغيير النقاط",
"reasonLabel": "السبب: "
},
"students": {
"title": "إدارة الطلاب",
"importList": "استيراد القائمة",
"addStudent": "إضافة طالب",
"name": "الاسم",
"group": "المجموعة",
"avatar": "الصورة",
"currentScore": "النقاط الحالية",
"tags": "الوسوم",
"noTags": "بدون وسوم",
"editTags": "تعديل الوسوم",
"editAvatar": "إعداد الصورة",
"deleteConfirm": "تأكيد حذف هذا الطالب؟",
"addTitle": "إضافة طالب",
"addConfirm": "إضافة",
"namePlaceholder": "أدخل اسم الطالب",
"groupPlaceholder": "أدخل اسم المجموعة (اختياري)",
"nameRequired": "الرجاء إدخال الاسم",
"nameExists": "اسم الطالب موجود بالفعل",
"addSuccess": "تمت الإضافة بنجاح",
"addFailed": "فشل الإضافة",
"deleteSuccess": "تم الحذف بنجاح",
"deleteFailed": "فشل الحذف",
"tagSaveSuccess": "تم حفظ الوسوم بنجاح",
"tagSaveFailed": "فشل حفظ الوسوم",
"importTitle": "استيراد القائمة",
"importTextHint": "الصق عدة أسطر، اسم طالب واحد لكل سطر",
"importTextPlaceholder": "مثال:\nمحمد\nفاطمة\nأحمد",
"importByText": "استيراد من النص",
"importByXlsx": "استيراد عبر xlsx",
"importByBanyou": "استيراد من BanYou",
"xlsxPreview": "معاينة واستيراد xlsx",
"file": "الملف",
"selectNameCol": "انقر على الرأس لتحديد عمود الأسماء",
"previewRows": "معاينة أول 50 سطر",
"importConfirm": "استيراد",
"importComplete": "اكتمل الاستيراد: تم إضافة {{inserted}}، تم تخطي {{skipped}}",
"workerNotReady": "العامل غير مهيأ",
"parseXlsxFailed": "فشل تحليل xlsx",
"selectNameColFirst": "الرجاء تحديد « عمود الأسماء » أولاً",
"noNamesFound": "لم يتم العثور على أسماء قابلة للاستيراد في العمود المحدد",
"importFailed": "فشل الاستيراد",
"banyouCookieHint": "بعد تسجيل الدخول إلى BanYou Web، انسخ ملف تعريف الارتباط الكامل والصقه أدناه.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "الرجاء لصق ملف تعريف الارتباط أولاً",
"banyouFetch": "الحصول على قائمة الفصول",
"banyouFetchSuccess": "تم الحصول على {{count}} فصول",
"banyouFetchFailed": "فشل الحصول على فصول BanYou",
"banyouCreatedClasses": "فصولي التي أنشأتها",
"banyouJoinedClasses": "فصولي التي انضممت إليها",
"banyouNoClasses": "لا توجد بيانات فصول"
},
"boards": {
"title": "إدارة اللوحات",
"addBoard": "إضافة لوحة",
"editBoard": "تعديل اللوحة",
"deleteBoard": "حذف اللوحة",
"name": "اسم اللوحة",
"description": "الوصف",
"visibility": "الرؤية",
"public": "عام",
"private": "خاص",
"studentsCount": "{{count}} طالب",
"noBoards": "لا توجد لوحات",
"addTitle": "إضافة لوحة",
"editTitle": "تعديل اللوحة",
"namePlaceholder": "أدخل اسم اللوحة",
"descriptionPlaceholder": "أدخل الوصف",
"confirmDelete": "تأكيد حذف هذه اللوحة؟",
"saved": "تم الحفظ",
"saveFailed": "فشل الحفظ",
"deleted": "تم الحذف",
"deleteFailed": "فشل الحذف",
"boardExists": "اللوحة موجودة بالفعل"
},
"leaderboard": {
"title": "لوحة الصدارة",
"sortBy": "الترتيب حسب",
"score": "النقاط",
"name": "الاسم",
"rank": "الترتيب",
"student": "الطالب",
"points": "النقاط",
"noData": "لا توجد بيانات ترتيب",
"topStudents": "أفضل الطلاب",
"rankChanged": "الترتيب: {{rank}}"
},
"settlements": {
"title": "سجل التسوية",
"phase": "المرحلة",
"date": "التاريخ",
"studentCount": "عدد الطلاب",
"totalPoints": "إجمالي النقاط",
"actions": "الإجراءات",
"viewDetails": "عرض التفاصيل",
"noSettlements": "لا توجد تسوية",
"phaseDetail": "تفاصيل المرحلة {{phase}}",
"settlementDate": "تاريخ التسوية: {{date}}",
"totalStudents": "إجمالي الطلاب: {{count}}",
"totalPointsChange": "إجمالي تغيير النقاط: {{points}}"
},
"reasons": {
"title": "إدارة الأسباب",
"addReason": "إضافة سبب",
"editReason": "تعديل السبب",
"name": "اسم السبب",
"points": "النقاط",
"positive": "إيجابي",
"negative": "سلبي",
"noReasons": "لا يوجد أسباب",
"addTitle": "إضافة سبب",
"editTitle": "تعديل السبب",
"namePlaceholder": "أدخل اسم السبب",
"pointsPlaceholder": "أدخل النقاط",
"confirmDelete": "تأكيد حذف هذا السبب؟",
"saved": "تم الحفظ",
"saveFailed": "فشل الحفظ",
"deleted": "تم الحذف",
"deleteFailed": "فشل الحذف",
"reasonExists": "السبب موجود بالفعل"
},
"autoScore": {
"title": "إضافة تلقائية للنقاط",
"addRule": "إضافة قاعدة",
"editRule": "تعديل القاعدة",
"deleteRule": "حذف القاعدة",
"ruleName": "اسم القاعدة",
"trigger": "المُحفز",
"action": "العملية",
"interval": "الفاصل الزمني",
"enabled": "مفعل",
"noRules": "لا توجد قواعد",
"addTitle": "إضافة قاعدة",
"editTitle": "تعديل القاعدة",
"namePlaceholder": "أدخل اسم القاعدة",
"triggerPlaceholder": "اختر المُحفز",
"actionPlaceholder": "اختر العملية",
"intervalPlaceholder": "اختر الفاصل الزمني",
"confirmDelete": "تأكيد حذف هذه القاعدة؟",
"saved": "تم الحفظ",
"saveFailed": "فشل الحفظ",
"deleted": "تم الحذف",
"deleteFailed": "فشل الحذف",
"ruleExists": "القاعدة موجودة بالفعل"
},
"rewardExchange": {
"title": "تبادل المكافآت",
"rewards": "المكافآت",
"exchange": "تبادل",
"history": "السجل",
"noRewards": "لا توجد مكافآت",
"noHistory": "لا يوجد سجل",
"rewardName": "اسم المكافأة",
"cost": "التكلفة",
"stock": "المخزون",
"exchangeSuccess": "تم التبادل بنجاح",
"exchangeFailed": "فشل التبادل",
"insufficientPoints": "نقاط غير كافية",
"outOfStock": "نفاد المخزون"
},
"rewardSettings": {
"title": "إعدادات المكافآت",
"addReward": "إضافة مكافأة",
"editReward": "تعديل المكافأة",
"deleteReward": "حذف المكافأة",
"name": "الاسم",
"cost": "التكلفة",
"stock": "المخزون",
"noRewards": "لا توجد مكافآت",
"addTitle": "إضافة مكافأة",
"editTitle": "تعديل المكافأة",
"namePlaceholder": "أدخل الاسم",
"costPlaceholder": "أدخل التكلفة",
"stockPlaceholder": "أدخل المخزون",
"confirmDelete": "تأكيد حذف هذه المكافأة؟",
"saved": "تم الحفظ",
"saveFailed": "فشل الحفظ",
"deleted": "تم الحذف",
"deleteFailed": "فشل الحذف",
"rewardExists": "المكافأة موجودة بالفعل"
},
"recovery": {
"title": "نص استرداد SecScore",
"saved": "لقد قمت بالحفظ",
"export": "تصدير ملف نصي",
"exportHint": "يوصى بالحفظ خارج الخط. في حالة الضياع، لا يمكن استعادته.",
"hint": "يوصى بالحفظ خارج الخط بعد التصدير. في حالة الضياع، لا يمكن استعادته."
}
}
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "Nächste",
"prev": "Vorherige",
"finish": "Fertig",
"cancel": "Abbrechen",
"save": "Speichern",
"loading": "Laden...",
"confirm": "Bestätigen",
"success": "Erfolg",
"error": "Fehler",
"delete": "Löschen",
"edit": "Bearbeiten",
"add": "Hinzufügen",
"search": "Suchen",
"clear": "Löschen",
"close": "Schließen",
"yes": "Ja",
"no": "Nein",
"submit": "Senden",
"reset": "Zurücksetzen",
"import": "Importieren",
"export": "Exportieren",
"create": "Erstellen",
"update": "Aktualisieren",
"refresh": "Aktualisieren",
"view": "Ansehen",
"name": "Name",
"status": "Status",
"operation": "Operation",
"description": "Beschreibung",
"total": "Insgesamt {{count}} Elemente",
"noData": "Keine Daten",
"pleaseSelect": "Bitte auswählen",
"pleaseEnter": "Bitte eingeben",
"readOnly": "Schreibgeschützter Modus",
"none": "Keine",
"more": "Mehr"
},
"oobe": {
"title": "Willkommen bei SecScore",
"subtitle": "Experte für Bildungs-Punktemanagement",
"step": "Schritt {{current}}/{{total}}",
"skip": "Überspringen",
"steps": {
"entry": {
"title": "Starten",
"description": "Wählen Sie das Verfahren aus",
"enterOobe": "In OOBE eintreten",
"connectPostgresAutoSync": "PostgreSQL mit automatischer Synchronisierung verbinden",
"skipDirect": "OOBE überspringen und direkt eintreten"
},
"postgresql": {
"title": "PostgreSQL verbinden",
"description": "Geben Sie die PostgreSQL-Verbindungszeichenfolge ein. Das System verbindet und synchronisiert automatisch.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "Automatisch verbinden und synchronisieren"
},
"language": {
"title": "Sprache auswählen",
"description": "Bitte wählen Sie Ihre bevorzugte Sprache"
},
"theme": {
"title": "Thema einstellen",
"description": "Wählen Sie das Erscheinungsbild der Schnittstelle",
"mode": "Modus",
"lightMode": "Hell",
"darkMode": "Dunkel",
"primaryColor": "Hauptfarbe",
"backgroundGradient": "Hintergrundfarbverlauf"
},
"password": {
"title": "Passwort einstellen",
"description": "Legen Sie ein Passwort zum Schutz Ihrer Daten fest (optional)",
"adminPassword": "Admin-Passwort",
"adminPasswordHint": "Zugriff auf alle Funktionen, 6 Ziffern",
"pointsPassword": "Punkte-Passwort",
"pointsPasswordHint": "Nur Punkte-Operationen, 6 Ziffern",
"passwordPlaceholder": "6 Ziffern eingeben",
"hint": "Leer lassen zum Überspringen, später in den Einstellungen konfigurieren"
},
"students": {
"title": "Schülerliste importieren",
"description": "Fügen Sie eine Schülerliste hinzu, um die Punktemanagement zu starten",
"import": "Liste importieren",
"manual": "Manuell hinzufügen",
"importHint": "Unterstützt Excel (.xlsx) oder JSON-Dateien",
"manualHint": "Mehrere Zeilen einfügen, ein Schülername pro Zeile",
"dragDrop": "Dateien hierher ziehen oder klicken zum Auswählen",
"supportedFormats": "Unterstützte Formate: .xlsx, .json",
"studentName": "Schülername",
"addStudent": "Schüler hinzufügen",
"noStudents": "Keine Schüler vorhanden, bitte hinzufügen oder importieren",
"studentCount": "{{count}} Schüler hinzugefügt",
"studentExists": "Schüler existiert bereits",
"importSuccess": "{{count}} Schüler erfolgreich importiert",
"noNewStudents": "Keine neuen Schüler zum Importieren",
"parseFailed": "Dateiparsing fehlgeschlagen",
"unsupportedFormat": "Nicht unterstütztes Dateiformat"
},
"reasons": {
"title": "Gründe einstellen",
"description": "Fügen Sie häufige Punkteänderungsgründe hinzu",
"addReason": "Grund hinzufügen",
"reasonName": "Grundname",
"points": "Punkte",
"positive": "Punkte hinzufügen",
"negative": "Punkte abziehen",
"noReasons": "Keine Gründe vorhanden, bitte häufige Gründe hinzufügen",
"reasonCount": "{{count}} Gründe hinzugefügt",
"reasonExists": "Grund existiert bereits"
},
"start": {
"title": "Bereit zur Verwendung",
"description": "Alles ist bereit. Lassen Sie uns SecScore verwenden!",
"features": {
"score": "Verwalten Sie Punkte der Schüler einfach",
"history": "Vollständige Historie der Punkteänderungen",
"settlement": "Unterstützt Abrechnung und Archivierung"
},
"startButton": "Starten"
}
}
},
"settings": {
"title": "Systemeinstellungen",
"tabs": {
"appearance": "Erscheinungsbild",
"security": "Sicherheit",
"account": "Konto",
"database": "Datenbankverbindung",
"dataManagement": "Datenverwaltung",
"urlProtocol": "URL-Protokoll",
"about": "Über"
},
"language": "Sprache",
"languageHint": "Wählen Sie die Sprache für die Schnittstellenanzeige",
"fontFamily": "Globale Schriftart",
"fontFamilyHint": "Wählen Sie die Schnittstellenschriftart (erfordert Neuladen)",
"theme": "Thema",
"password": "Passwort einstellen",
"themeMode": "Modus",
"lightMode": "Hell",
"darkMode": "Dunkel",
"primaryColor": "Hauptfarbe",
"backgroundGradient": "Hintergrundfarbverlauf",
"searchKeyboard": {
"title": "Suchtastatur",
"hint": "Wählen Sie das Tastaturlayout unter dem Suchfeld",
"disableToggle": "Suchtastatur deaktivieren",
"disableHint": "Die Tastatur wird nicht mehr unter dem Suchfeld angezeigt",
"options": {
"qwerty26": "26 Tasten (QWERTY)",
"t9": "9 Tasten (T9)"
}
},
"interfaceZoom": "Schnittstellenzoom",
"zoomOptions": {
"small70": "Klein (70%)",
"default100": "Standard (100%)",
"large150": "Groß (150%)"
},
"general": {
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen"
},
"zoomHint": "Gesamtgröße der Schnittstelle anpassen",
"mobile": {
"navigationTitle": "Seiteneingänge",
"bottomNav": {
"label": "Funktionen der unteren Navigationsleiste",
"hint": "Wählen Sie die Funktionen aus, die in der unteren Leiste des Mobiltelefons angezeigt werden sollen. Die Leiste zeigt maximal 4 Elemente an, die anderen gehen in «Mehr»."
},
"dragHint": "Zum Neuordnen ziehen. Die ersten 4 Elemente erscheinen in der unteren Leiste, die anderen in «Mehr».",
"dropHere": "Hier loslassen",
"bottomSection": "Untere Leiste (max. 4)",
"moreSection": "Mehr",
"moveToMore": "Zu Mehr verschieben",
"moveToBottom": "Nach unten verschieben"
},
"zoomUpdated": "Schnittstellenzoom aktualisiert",
"updateFailed": "Aktualisierung fehlgeschlagen",
"security": {
"title": "Passwortschutzsystem",
"adminPassword": "Admin-Passwort",
"pointsPassword": "Punkte-Passwort",
"recoveryString": "Wiederherstellungszeichenfolge",
"set": "Eingestellt",
"notSet": "Nicht eingestellt",
"generated": "Erstellt",
"notGenerated": "Nicht erstellt",
"enterPassword": "6 Ziffern eingeben (leer lassen zum Überspringen)",
"adminPasswordPlaceholder": "6 Ziffern Admin-Passwort eingeben (leer lassen zum Überspringen)",
"pointsPasswordPlaceholder": "6 Ziffern Punkte-Passwort eingeben (leer lassen zum Überspringen)",
"recoveryPlaceholder": "Wiederherstellungszeichenfolge eingeben",
"savePassword": "Passwort speichern",
"savePasswords": "Passwörter speichern",
"generateRecovery": "Wiederherstellungszeichenfolge generieren",
"clearAllPasswords": "Alle Passwörter löschen",
"recoveryReset": "Wiederherstellungszeichenfolge zurücksetzen",
"enterRecovery": "Wiederherstellungszeichenfolge eingeben",
"resetPassword": "Passwort zurücksetzen",
"resetHint": "Das Zurücksetzen löscht die Admin- und Punkte-Passwörter und generiert eine neue Wiederherstellungszeichenfolge.",
"confirmClear": "Alle Passwörter löschen bestätigen?",
"clearHint": "Nach dem Löschen wird der Schutz deaktiviert (ohne Passwort = Standard-Admin-Berechtigung).",
"saved": "Passwort aktualisiert",
"saveFailed": "Aktualisierung fehlgeschlagen",
"generateFailed": "Generierung fehlgeschlagen",
"resetFailed": "Zurücksetzen fehlgeschlagen",
"cleared": "Gelöscht",
"clearFailed": "Löschen fehlgeschlagen",
"passwordClearedNewRecovery": "Passwort gelöscht, neue Wiederherstellungszeichenfolge",
"newRecoveryString": "Neue Wiederherstellungszeichenfolge (bitte speichern)"
},
"account": {
"title": "SECTL Auth-Konto",
"notLoggedIn": "Nicht bei SECTL Auth angemeldet",
"oauthHint": "Melden Sie sich mit Ihrem SECTL Auth-Konto an, um die einheitliche Authentifizierung und Remote-Abmeldung zu nutzen",
"loginButton": "Mit SECTL Auth anmelden",
"loginSuccess": "Anmeldung erfolgreich",
"logout": "Abmelden",
"userId": "Benutzer-ID",
"permission": "Berechtigungsstufe"
},
"database": {
"title": "Datenbankverbindung",
"currentStatus": "Aktueller Datenbankstatus",
"sqliteLocal": "Lokale SQLite-Datenbank",
"postgresqlRemote": "Remote-PostgreSQL-Datenbank",
"connected": "Verbunden",
"disconnected": "Getrennt",
"postgresqlConnection": "Remote-PostgreSQL-Verbindung",
"connectionHint": "Geben Sie die PostgreSQL-Verbindungszeichenfolge ein, um eine Remote-Datenbank zu verbinden, Unterstützung für Multi-Plattform-Synchronisierung.",
"testConnection": "Verbindung testen",
"switchToPostgreSQL": "Zu PostgreSQL wechseln",
"switchToSQLite": "Zu lokalem SQLite wechseln",
"connectionTestSuccess": "Verbindungstest erfolgreich",
"connectionTestFailed": "Verbindungstest fehlgeschlagen",
"switchedTo": "Zu Datenbank {{type}} gewechselt",
"switchedToSQLite": "Zu lokaler SQLite-Datenbank gewechselt",
"switchFailed": "Wechsel fehlgeschlagen",
"syncDescription": "Anweisungen für Multi-Plattform-Synchronisierung",
"syncPoint1": "Die Verwendung einer Remote-PostgreSQL-Datenbank ermöglicht die Synchronisierung von Daten auf mehreren Plattformen.",
"syncPoint2": "Integrierter Operations-Warteschlangen-Mechanismus zur Gewährleistung der Datenkonsistenz bei gleichzeitigen Operationen.",
"syncPoint3": "Nach dem Wechsel der Datenbank ist ein Neustart der Anwendung erforderlich.",
"syncPoint4": "Empfohlene Cloud-Datenbankdienste: Neon, Supabase, AWS RDS usw.",
"enterConnectionString": "Bitte PostgreSQL-Verbindungszeichenfolge eingeben",
"connectionExample": "Beispiel: postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "Lokale Daten auf Remote-Server hochladen",
"uploadHint": "Nach der Verbindung mit der Remote-Datenbank laden Sie den lokalen SQLite-Verlauf auf den Remote-Server hoch und fusionieren ihn.",
"uploadNeedRemote": "Bitte zuerst zu Remote-PostgreSQL-Datenbank wechseln und verbinden",
"noNeedUpload": "Lokale und Remote-Daten sind bereits konsistent",
"uploadConfirmTitle": "Lokale Daten auf Remote-Server hochladen bestätigen?",
"uploadConfirmContent": "Die Synchronisierung priorisiert lokale Daten. Erkannt: nur lokal {{localOnly}}, nur remote {{remoteOnly}}, Konflikte {{conflicts}}.",
"uploadSuccess": "Lokale Daten auf Remote-Server hochgeladen",
"uploadFailed": "Hochladen fehlgeschlagen"
},
"data": {
"title": "Datenverwaltung",
"settlement": "Abrechnung",
"settlementAndRestart": "Abrechnen und neu starten",
"settlementHint": "Klassifiziert unabgerechnete Aufzeichnungen in eine Phase und setzt alle Schülerpunkte auf null zurück.",
"importExport": "Importieren / Exportieren",
"exportJson": "JSON exportieren",
"importJson": "JSON importieren",
"importHint": "Der Import überschreibt die vorhandenen Schüler/Gründe/Aufzeichnungen/Einstellungen (Sicherheitseinstellungen ausgenommen).",
"logs": "Protokolle",
"logLevel": "Protokollebene",
"logOperation": "Protokolloperation",
"viewLogs": "Protokolle anzeigen",
"exportLogs": "Protokolle exportieren",
"clearLogs": "Protokolle löschen",
"logsCleared": "Protokolle gelöscht",
"systemLogs": "Systemprotokolle (letzte 200)",
"noLogs": "Keine Protokolle",
"logLevelUpdated": "Protokollebene aktualisiert",
"readLogsFailed": "Protokolle lesen fehlgeschlagen",
"logsExported": "Protokolle exportiert",
"exportFailed": "Export fehlgeschlagen",
"exportSuccess": "Export erfolgreich",
"importSuccess": "Import erfolgreich, aktualisieren",
"importFailed": "Import fehlgeschlagen",
"clearFailed": "Löschen fehlgeschlagen",
"confirmSettlement": "Abrechnung und Neustart bestätigen?",
"settlementConfirm1": "Dies archiviert unabgerechnete Aufzeichnungen in einer Phase und setzt alle Schülerpunkte auf null zurück.",
"settlementConfirm2": "Die Schülerliste bleibt unverändert; die Abrechnungshistorie ist auf der Seite «Abrechnungshistorie» abrufbar.",
"settlementSuccess": "Abrechnung erfolgreich, Punkte neu gestartet",
"settlementFailed": "Abrechnung fehlgeschlagen",
"settle": "Abrechnen",
"logLevels": {
"debug": "DEBUG (Debuggen)",
"info": "INFO (Information)",
"warn": "WARN (Warnung)",
"error": "ERROR (Fehler)"
}
},
"url": {
"title": "URL-Protokoll (secscore://)",
"protocol": "URL-Protokoll",
"description": "Sie können SecScore über eine URL aufrufen, um Operationen auszuführen, z. B.:",
"register": "URL-Protokoll registrieren",
"registered": "URL-Protokoll registriert",
"registerFailed": "Registrierung fehlgeschlagen",
"installerRequired": "Erfordert die installierte Version von SecScore, kann im Entwicklungsmodus nicht funktionieren."
},
"mcp": {
"title": "MCP-Dienst",
"description": "Verwalten Sie den integrierten MCP HTTP-Dienst für externe MCP-Clients.",
"running": "Läuft",
"stopped": "Gestoppt",
"noUrl": "Keine URL",
"host": "Host",
"port": "Port",
"start": "Starten",
"stop": "Stoppen",
"refresh": "Status aktualisieren",
"hint": "Empfohlen, nur an 127.0.0.1 zu binden; nach dem Start ist der Endpunkt http://host:port/mcp",
"hostRequired": "Bitte MCP-Host eingeben",
"portInvalid": "Bitte gültigen Port eingeben (1-65535)",
"startSuccess": "MCP-Server gestartet",
"startFailed": "Starten des MCP-Servers fehlgeschlagen",
"stopSuccess": "MCP-Server gestoppt",
"stopFailed": "Stoppen des MCP-Servers fehlgeschlagen",
"startTimeout": "Zeitüberschreitung beim Starten des MCP-Servers, bitte erneut versuchen",
"stopTimeout": "Zeitüberschreitung beim Stoppen des MCP-Servers, bitte erneut versuchen"
},
"app": {
"title": "Anwendungssteuerung",
"description": "Anwendung schnell neu starten oder schließen.",
"restart": "Anwendung neu starten",
"quit": "Anwendung schließen",
"confirmRestartTitle": "Neustart der Anwendung bestätigen?",
"confirmRestartContent": "Die Anwendung wird sofort geschlossen und neu gestartet.",
"confirmQuitTitle": "Schließen der Anwendung bestätigen?",
"confirmQuitContent": "Die Anwendung wird sofort geschlossen.",
"restartFailed": "Neustart fehlgeschlagen",
"quitFailed": "Schließen fehlgeschlagen"
},
"about": {
"title": "Über",
"appName": "Bildungs-Punktemanagement",
"version": "Version",
"copyright": "Urheberrechte",
"ipcStatus": "IPC-Status",
"ipcConnected": "Verbunden",
"ipcDisconnected": "Getrennt (Vorladen fehlgeschlagen)",
"environment": "Umgebung",
"toggleDevTools": "Entwicklerwerkzeuge umschalten",
"license": "SecScore unterliegt der GPL3.0-Lizenz",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "Startseite",
"students": "Schülerverwaltung",
"score": "Punkteoperationen",
"boards": "Boards",
"leaderboard": "Bestenliste",
"settlements": "Abrechnungshistorie",
"reasons": "Grundverwaltung",
"autoScore": "Automatische Punktehinzufügung",
"rewardExchange": "Belohnungsaustausch",
"rewardSettings": "Belohnungseinstellungen",
"settings": "Systemeinstellungen",
"remoteDb": "Remote-Datenbank",
"refreshStatus": "Status aktualisieren",
"syncNow": "Jetzt synchronisieren",
"syncSuccess": "Synchronisierung erfolgreich",
"syncFailed": "Synchronisierung fehlgeschlagen",
"getDbStatusFailed": "Datenbankstatus abrufen fehlgeschlagen",
"notRemoteMode": "Remote-Datenbankmodus nicht aktiviert, Anwendung neu starten",
"dbNotConnected": "Datenbank nicht verbunden"
},
"auth": {
"unlock": "Berechtigung entsperren",
"unlockHint": "6-stelliges Passwort eingeben: Admin-Passwort = voller Zugriff, Punkte-Passwort = nur Punkteoperationen.",
"unlockButton": "Entsperren",
"unlocked": "Berechtigung entsperrt",
"logout": "Auf Nur-Lese-Modus umgeschaltet",
"passwordPlaceholder": "z. B. 123456",
"passwordError": "Falsches Passwort",
"enterPassword": "Passwort eingeben",
"lock": "Sperren"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "Farbe und Hintergrund",
"gradientLabels": {
"blue": "Frisches Blau",
"pink": "Weicher Rosa",
"cyan": "Cyan",
"purple": "Violett"
},
"gradientDirections": {
"vertical": "Vertikal",
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"generate": "Generieren",
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen",
"mode": "Modus",
"myTheme": "Mein Thema"
},
"permissions": {
"admin": "Admin-Berechtigung",
"points": "Punkte-Berechtigung",
"view": "Nur-Lese"
},
"home": {
"title": "Startseite der Schülerpunkte",
"subtitle": "Insgesamt {{count}} Schüler, klicken Sie auf die Karte, um Operationen durchzuführen",
"searchPlaceholder": "Name/Pinyin suchen...",
"sortBy": {
"alphabet": "Sortieren nach Name",
"surname": "Gruppe nach Nachname",
"group": "Sortieren nach Gruppe",
"score": "Rangfolge nach Punkten"
},
"layoutBy": {
"grouped": "Gruppierte Liste",
"squareGrid": "Quadratisches Raster"
},
"noStudents": "Keine Schülerdaten vorhanden, bitte in der Schülerverwaltung hinzufügen",
"noMatch": "Keine übereinstimmenden Schüler gefunden",
"clearSearch": "Suche löschen",
"points": "Punkte",
"currentScore": "Aktuelle Punkte",
"quickOptions": "Schnelloptionen",
"adjustPoints": "Punkte anpassen",
"customPoints": "Benutzerdefinierte Punkte",
"customPointsHint": "Beliebigen Wert im Eingabefeld eingeben",
"reason": "Grund",
"reasonPlaceholder": "Grund für die Punkteänderung eingeben (optional)",
"preview": "Änderungsvorschau",
"noReason": "(Kein Grund)",
"ungrouped": "Nicht gruppiert",
"category": {
"others": "Andere"
},
"scoreAdded": "{{points}} Punkte zu {{name}} hinzugefügt",
"scoreDeducted": "{{points}} Punkte von {{name}} abgezogen",
"submitFailed": "Senden fehlgeschlagen",
"pleaseSelectPoints": "Bitte Punkte auswählen oder eingeben",
"reasonDefault": "{{action}} {{points}} Punkte",
"studentCount": "{{count}} Schüler",
"operationTitle": "Punkteoperation: {{name}}",
"submitOperation": "Operation senden",
"operationTitleBatch": "Punkteoperation: {{count}} ausgewählt",
"undoLastAction": "Rückgängig",
"undoLastHint": "Letzte Aktion rückgängig: {{name}} {{delta}}",
"undoUnavailable": "Keine Punkteoperation zum Rückgängig machen",
"undoLastSuccess": "Letzte Punkteoperation rückgängig gemacht",
"multiSelect": "Mehrfachauswahl",
"batchMode": "Batch-Modus",
"selectAll": "Alle auswählen",
"clearSelected": "Auswahl löschen",
"batchOperate": "Batch-Punkte",
"selected": "Ausgewählt",
"selectedCount": "{{count}} ausgewählt",
"batchSuccess": "Punkte für {{count}} Schüler gesendet",
"batchPartial": "{{success}}/{{total}} Schüler erfolgreich gesendet",
"selectStudentFirst": "Bitte wählen Sie zuerst einen oder mehrere Schüler aus",
"addPoints": "Punkte hinzufügen",
"deductPoints": "Punkte abziehen",
"pointsChange": "Punkteänderung",
"reasonLabel": "Grund: "
},
"students": {
"title": "Schülerverwaltung",
"importList": "Liste importieren",
"addStudent": "Schüler hinzufügen",
"name": "Name",
"group": "Gruppe",
"avatar": "Avatar",
"currentScore": "Aktuelle Punkte",
"tags": "Tags",
"noTags": "Keine Tags",
"editTags": "Tags bearbeiten",
"editAvatar": "Avatar einstellen",
"deleteConfirm": "Diesen Schüler löschen bestätigen?",
"addTitle": "Schüler hinzufügen",
"addConfirm": "Hinzufügen",
"namePlaceholder": "Schülernamen eingeben",
"groupPlaceholder": "Gruppennamen eingeben (optional)",
"nameRequired": "Bitte Namen eingeben",
"nameExists": "Schülernamen existiert bereits",
"addSuccess": "Hinzugefügt erfolgreich",
"addFailed": "Hinzufügen fehlgeschlagen",
"deleteSuccess": "Gelöscht erfolgreich",
"deleteFailed": "Löschen fehlgeschlagen",
"tagSaveSuccess": "Tags gespeichert erfolgreich",
"tagSaveFailed": "Speichern von Tags fehlgeschlagen",
"importTitle": "Liste importieren",
"importTextHint": "Mehrere Zeilen einfügen, ein Schülername pro Zeile",
"importTextPlaceholder": "Beispiel:\nHans\nMaria\nPeter",
"importByText": "Aus Text importieren",
"importByXlsx": "Via xlsx importieren",
"importByBanyou": "Aus BanYou importieren",
"xlsxPreview": "xlsx-Vorschau und Import",
"file": "Datei",
"selectNameCol": "Klicken Sie auf die Kopfzeile, um die Namensspalte auszuwählen",
"previewRows": "Vorschau der ersten 50 Zeilen",
"importConfirm": "Importieren",
"importComplete": "Import abgeschlossen: {{inserted}} hinzugefügt, {{skipped}} übersprungen",
"workerNotReady": "Worker nicht initialisiert",
"parseXlsxFailed": "xlsx-Parsing fehlgeschlagen",
"selectNameColFirst": "Bitte wählen Sie zuerst die «Namensspalte» aus",
"noNamesFound": "Keine importierbaren Namen in der ausgewählten Spalte gefunden",
"importFailed": "Import fehlgeschlagen",
"banyouCookieHint": "Nach dem Anmelden bei BanYou Web kopieren Sie das vollständige Cookie und fügen es unten ein.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "Bitte zuerst Cookie einfügen",
"banyouFetch": "Klasseliste abrufen",
"banyouFetchSuccess": "{{count}} Klassen abgerufen",
"banyouFetchFailed": "Abrufen von BanYou-Klassen fehlgeschlagen",
"banyouCreatedClasses": "Meine erstellten Klassen",
"banyouJoinedClasses": "Meine beigetretenen Klassen",
"banyouNoClasses": "Keine Klassendaten vorhanden"
},
"boards": {
"title": "Boardverwaltung",
"addBoard": "Board hinzufügen",
"editBoard": "Board bearbeiten",
"deleteBoard": "Board löschen",
"name": "Boardname",
"description": "Beschreibung",
"visibility": "Sichtbarkeit",
"public": "Öffentlich",
"private": "Privat",
"studentsCount": "{{count}} Schüler",
"noBoards": "Keine Boards vorhanden",
"addTitle": "Board hinzufügen",
"editTitle": "Board bearbeiten",
"namePlaceholder": "Boardname eingeben",
"descriptionPlaceholder": "Beschreibung eingeben",
"confirmDelete": "Dieses Board löschen bestätigen?",
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen",
"deleted": "Gelöscht",
"deleteFailed": "Löschen fehlgeschlagen",
"boardExists": "Board existiert bereits"
},
"leaderboard": {
"title": "Bestenliste",
"sortBy": "Sortieren nach",
"score": "Punkte",
"name": "Name",
"rank": "Rang",
"student": "Schüler",
"points": "Punkte",
"noData": "Keine Ranglisten-Daten vorhanden",
"topStudents": "Beste Schüler",
"rankChanged": "Rang: {{rank}}"
},
"settlements": {
"title": "Abrechnungshistorie",
"phase": "Phase",
"date": "Datum",
"studentCount": "Anzahl der Schüler",
"totalPoints": "Gesamtpunkte",
"actions": "Aktionen",
"viewDetails": "Details anzeigen",
"noSettlements": "Keine Abrechnungen vorhanden",
"phaseDetail": "Details der Phase {{phase}}",
"settlementDate": "Abrechnungsdatum: {{date}}",
"totalStudents": "Gesamtzahl der Schüler: {{count}}",
"totalPointsChange": "Gesamte Punkteänderung: {{points}}"
},
"reasons": {
"title": "Grundverwaltung",
"addReason": "Grund hinzufügen",
"editReason": "Grund bearbeiten",
"name": "Grundname",
"points": "Punkte",
"positive": "Positiv",
"negative": "Negativ",
"noReasons": "Keine Gründe vorhanden",
"addTitle": "Grund hinzufügen",
"editTitle": "Grund bearbeiten",
"namePlaceholder": "Grundname eingeben",
"pointsPlaceholder": "Punkte eingeben",
"confirmDelete": "Diesen Grund löschen bestätigen?",
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen",
"deleted": "Gelöscht",
"deleteFailed": "Löschen fehlgeschlagen",
"reasonExists": "Grund existiert bereits"
},
"autoScore": {
"title": "Automatische Punktehinzufügung",
"addRule": "Regel hinzufügen",
"editRule": "Regel bearbeiten",
"deleteRule": "Regel löschen",
"ruleName": "Regelname",
"trigger": "Auslöser",
"action": "Aktion",
"interval": "Intervall",
"enabled": "Aktiviert",
"noRules": "Keine Regeln vorhanden",
"addTitle": "Regel hinzufügen",
"editTitle": "Regel bearbeiten",
"namePlaceholder": "Regelname eingeben",
"triggerPlaceholder": "Auslöser auswählen",
"actionPlaceholder": "Aktion auswählen",
"intervalPlaceholder": "Intervall auswählen",
"confirmDelete": "Diese Regel löschen bestätigen?",
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen",
"deleted": "Gelöscht",
"deleteFailed": "Löschen fehlgeschlagen",
"ruleExists": "Regel existiert bereits"
},
"rewardExchange": {
"title": "Belohnungsaustausch",
"rewards": "Belohnungen",
"exchange": "Austauschen",
"history": "Historie",
"noRewards": "Keine Belohnungen vorhanden",
"noHistory": "Keine Historie vorhanden",
"rewardName": "Belohnungsname",
"cost": "Kosten",
"stock": "Bestand",
"exchangeSuccess": "Austausch erfolgreich",
"exchangeFailed": "Austausch fehlgeschlagen",
"insufficientPoints": "Unzureichende Punkte",
"outOfStock": "Nicht auf Lager"
},
"rewardSettings": {
"title": "Belohnungseinstellungen",
"addReward": "Belohnung hinzufügen",
"editReward": "Belohnung bearbeiten",
"deleteReward": "Belohnung löschen",
"name": "Name",
"cost": "Kosten",
"stock": "Bestand",
"noRewards": "Keine Belohnungen vorhanden",
"addTitle": "Belohnung hinzufügen",
"editTitle": "Belohnung bearbeiten",
"namePlaceholder": "Name eingeben",
"costPlaceholder": "Kosten eingeben",
"stockPlaceholder": "Bestand eingeben",
"confirmDelete": "Diese Belohnung löschen bestätigen?",
"saved": "Gespeichert",
"saveFailed": "Speichern fehlgeschlagen",
"deleted": "Gelöscht",
"deleteFailed": "Löschen fehlgeschlagen",
"rewardExists": "Belohnung existiert bereits"
},
"recovery": {
"title": "SecScore-Wiederherstellungszeichenfolge",
"saved": "Ich habe gespeichert",
"export": "Textdatei exportieren",
"exportHint": "Es wird empfohlen, offline zu speichern. Bei Verlust kann es nicht wiederhergestellt werden.",
"hint": "Es wird empfohlen, nach dem Export offline zu speichern. Bei Verlust kann es nicht wiederhergestellt werden."
}
}
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "Siguiente",
"prev": "Anterior",
"finish": "Finalizar",
"cancel": "Cancelar",
"save": "Guardar",
"loading": "Cargando...",
"confirm": "Confirmar",
"success": "Éxito",
"error": "Error",
"delete": "Eliminar",
"edit": "Editar",
"add": "Añadir",
"search": "Buscar",
"clear": "Borrar",
"close": "Cerrar",
"yes": "Sí",
"no": "No",
"submit": "Enviar",
"reset": "Reiniciar",
"import": "Importar",
"export": "Exportar",
"create": "Crear",
"update": "Actualizar",
"refresh": "Actualizar",
"view": "Ver",
"name": "Nombre",
"status": "Estado",
"operation": "Operación",
"description": "Descripción",
"total": "Total {{count}} elementos",
"noData": "Sin datos",
"pleaseSelect": "Por favor seleccione",
"pleaseEnter": "Por favor ingrese",
"readOnly": "Modo de solo lectura",
"none": "Ninguno",
"more": "Más"
},
"oobe": {
"title": "Bienvenido a SecScore",
"subtitle": "Experto en gestión de puntos educativos",
"step": "Paso {{current}}/{{total}}",
"skip": "Saltar",
"steps": {
"entry": {
"title": "Comenzar",
"description": "Elija el procedimiento a seguir",
"enterOobe": "Entrar en el OOBE",
"connectPostgresAutoSync": "Conectar PostgreSQL con sincronización automática",
"skipDirect": "Saltar OOBE y entrar directamente"
},
"postgresql": {
"title": "Conectar PostgreSQL",
"description": "Ingrese la cadena de conexión PostgreSQL. El sistema se conectará y sincronizará automáticamente.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "Conectar y sincronizar automáticamente"
},
"language": {
"title": "Elegir idioma",
"description": "Por favor elija su idioma preferido"
},
"theme": {
"title": "Configurar tema",
"description": "Elija la apariencia de la interfaz",
"mode": "Modo",
"lightMode": "Claro",
"darkMode": "Oscuro",
"primaryColor": "Color principal",
"backgroundGradient": "Degradado de fondo"
},
"password": {
"title": "Configurar contraseña",
"description": "Configure una contraseña para proteger sus datos (opcional)",
"adminPassword": "Contraseña de administrador",
"adminPasswordHint": "Acceso a todas las funciones, 6 dígitos",
"pointsPassword": "Contraseña de puntos",
"pointsPasswordHint": "Operaciones de puntos únicamente, 6 dígitos",
"passwordPlaceholder": "Ingrese 6 dígitos",
"hint": "Dejar vacío para omitir, configurar más tarde en configuraciones"
},
"students": {
"title": "Importar lista de estudiantes",
"description": "Agregue una lista de estudiantes para comenzar la gestión de puntos",
"import": "Importar lista",
"manual": "Añadir manualmente",
"importHint": "Soporta archivos Excel (.xlsx) o JSON",
"manualHint": "Pegue varias líneas, un nombre de estudiante por línea",
"dragDrop": "Arrastre y suelte archivos aquí o haga clic para seleccionar",
"supportedFormats": "Formatos soportados: .xlsx, .json",
"studentName": "Nombre del estudiante",
"addStudent": "Añadir estudiante",
"noStudents": "Sin estudiantes por el momento, por favor añada o importe",
"studentCount": "{{count}} estudiantes añadidos",
"studentExists": "El estudiante ya existe",
"importSuccess": "{{count}} estudiantes importados con éxito",
"noNewStudents": "Sin nuevos estudiantes para importar",
"parseFailed": "Fallo al analizar el archivo",
"unsupportedFormat": "Formato de archivo no soportado"
},
"reasons": {
"title": "Configurar razones",
"description": "Agregue las razones comunes de modificación de puntos",
"addReason": "Añadir razón",
"reasonName": "Nombre de la razón",
"points": "Puntos",
"positive": "Añadir puntos",
"negative": "Retirar puntos",
"noReasons": "Sin razones por el momento, por favor agregue razones comunes",
"reasonCount": "{{count}} razones añadidas",
"reasonExists": "La razón ya existe"
},
"start": {
"title": "Listo para usar",
"description": "Todo está listo. ¡Comencemos a usar SecScore!",
"features": {
"score": "Gestione fácilmente los puntos de los estudiantes",
"history": "Historial completo de modificaciones de puntos",
"settlement": "Soporta liquidación y archivado"
},
"startButton": "Comenzar"
}
}
},
"settings": {
"title": "Configuraciones del sistema",
"tabs": {
"appearance": "Apariencia",
"security": "Seguridad",
"account": "Cuenta",
"database": "Conexión a base de datos",
"dataManagement": "Gestión de datos",
"urlProtocol": "Protocolo URL",
"about": "Acerca de"
},
"language": "Idioma",
"languageHint": "Elegir el idioma de visualización de la interfaz",
"fontFamily": "Fuente global",
"fontFamilyHint": "Elegir la fuente de la interfaz (requiere actualización)",
"theme": "Tema",
"password": "Configurar contraseña",
"themeMode": "Modo",
"lightMode": "Claro",
"darkMode": "Oscuro",
"primaryColor": "Color principal",
"backgroundGradient": "Degradado de fondo",
"searchKeyboard": {
"title": "Teclado de búsqueda",
"hint": "Elegir la disposición del teclado bajo el campo de búsqueda",
"disableToggle": "Desactivar teclado de búsqueda",
"disableHint": "El teclado ya no se mostrará bajo el campo de búsqueda",
"options": {
"qwerty26": "26 teclas (QWERTY)",
"t9": "9 teclas (T9)"
}
},
"interfaceZoom": "Zoom de la interfaz",
"zoomOptions": {
"small70": "Pequeño (70%)",
"default100": "Predeterminado (100%)",
"large150": "Grande (150%)"
},
"general": {
"saved": "Guardado",
"saveFailed": "Fallo al guardar"
},
"zoomHint": "Ajustar el tamaño global de la interfaz",
"mobile": {
"navigationTitle": "Entradas de página",
"bottomNav": {
"label": "Funciones de navegación inferior",
"hint": "Elegir las funciones a mostrar en la barra inferior del móvil. La barra muestra un máximo de 4 elementos, los demás van en «Más»."
},
"dragHint": "Arrastre para reorganizar. Los primeros 4 elementos aparecen en la barra inferior, los demás en «Más».",
"dropHere": "Suelte aquí",
"bottomSection": "Barra inferior (máx. 4)",
"moreSection": "Más",
"moveToMore": "Mover a Más",
"moveToBottom": "Mover abajo"
},
"zoomUpdated": "Zoom de la interfaz actualizado",
"updateFailed": "Fallo al actualizar",
"security": {
"title": "Sistema de protección por contraseña",
"adminPassword": "Contraseña de administrador",
"pointsPassword": "Contraseña de puntos",
"recoveryString": "Frase de recuperación",
"set": "Configurado",
"notSet": "No configurado",
"generated": "Generado",
"notGenerated": "No generado",
"enterPassword": "Ingrese 6 dígitos (dejar vacío para no modificar)",
"adminPasswordPlaceholder": "Ingrese 6 dígitos para la contraseña de administrador (dejar vacío para no modificar)",
"pointsPasswordPlaceholder": "Ingrese 6 dígitos para la contraseña de puntos (dejar vacío para no modificar)",
"recoveryPlaceholder": "Ingrese la frase de recuperación",
"savePassword": "Guardar contraseña",
"savePasswords": "Guardar contraseñas",
"generateRecovery": "Generar frase de recuperación",
"clearAllPasswords": "Borrar todas las contraseñas",
"recoveryReset": "Reiniciar frase de recuperación",
"enterRecovery": "Ingrese la frase de recuperación",
"resetPassword": "Reiniciar contraseña",
"resetHint": "El reinicio borrará las contraseñas de administrador y puntos, y generará una nueva frase de recuperación.",
"confirmClear": "¿Confirmar el borrado de todas las contraseñas?",
"clearHint": "Después del borrado, la protección se desactivará (sin contraseña = permiso de administrador por defecto).",
"saved": "Contraseña actualizada",
"saveFailed": "Fallo al actualizar",
"generateFailed": "Fallo al generar",
"resetFailed": "Fallo al reiniciar",
"cleared": "Borrado",
"clearFailed": "Fallo al borrar",
"passwordClearedNewRecovery": "Contraseña borrada, nueva frase de recuperación",
"newRecoveryString": "Nueva frase de recuperación (por favor guárdela)"
},
"account": {
"title": "Cuenta SECTL Auth",
"notLoggedIn": "No conectado a SECTL Auth",
"oauthHint": "Conéctese con su cuenta SECTL Auth para disfrutar de la autenticación unificada y la desconexión remota",
"loginButton": "Conectar con SECTL Auth",
"loginSuccess": "Conexión exitosa",
"logout": "Desconectar",
"userId": "ID de usuario",
"permission": "Nivel de permiso"
},
"database": {
"title": "Conexión a base de datos",
"currentStatus": "Estado actual de la base de datos",
"sqliteLocal": "Base de datos SQLite local",
"postgresqlRemote": "Base de datos PostgreSQL remota",
"connected": "Conectado",
"disconnected": "Desconectado",
"postgresqlConnection": "Conexión PostgreSQL remota",
"connectionHint": "Ingrese la cadena de conexión PostgreSQL para conectarse a una base de datos remota, soporta sincronización multiplataforma.",
"testConnection": "Probar conexión",
"switchToPostgreSQL": "Cambiar a PostgreSQL",
"switchToSQLite": "Cambiar a SQLite local",
"connectionTestSuccess": "Prueba de conexión exitosa",
"connectionTestFailed": "Fallo en la prueba de conexión",
"switchedTo": "Cambiado a la base de datos {{type}}",
"switchedToSQLite": "Cambiado a la base de datos SQLite local",
"switchFailed": "Fallo al cambiar",
"syncDescription": "Instrucciones de sincronización multiplataforma",
"syncPoint1": "El uso de una base de datos PostgreSQL remota permite la sincronización de datos multiplataforma.",
"syncPoint2": "Mecanismo de cola de operaciones integrado para garantizar la coherencia de los datos durante las operaciones simultáneas.",
"syncPoint3": "Es necesario reiniciar la aplicación después del cambio de base de datos.",
"syncPoint4": "Servicios de base de datos en la nube recomendados: Neon, Supabase, AWS RDS, etc.",
"enterConnectionString": "Por favor ingrese la cadena de conexión PostgreSQL",
"connectionExample": "Ejemplo: postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "Cargar datos locales al servidor remoto",
"uploadHint": "Después de conectarse a la base de datos remota, cargue y fusione el historial SQLite local al servidor remoto.",
"uploadNeedRemote": "Por favor primero cambie y conecte a la base de datos PostgreSQL remota",
"noNeedUpload": "Los datos locales y remotos ya son coherentes",
"uploadConfirmTitle": "¿Confirmar la carga de datos locales al servidor remoto?",
"uploadConfirmContent": "La sincronización priorizará los datos locales. Detectado: local-solo {{localOnly}}, remoto-solo {{remoteOnly}}, conflictos {{conflicts}}.",
"uploadSuccess": "Datos locales cargados al servidor remoto",
"uploadFailed": "Fallo al cargar"
},
"data": {
"title": "Gestión de datos",
"settlement": "Liquidación",
"settlementAndRestart": "Liquidar y reiniciar",
"settlementHint": "Clasifica los registros no liquidados en una fase y restablece todos los puntos de los estudiantes a cero.",
"importExport": "Importar / Exportar",
"exportJson": "Exportar JSON",
"importJson": "Importar JSON",
"importHint": "La importación sobrescribirá los estudiantes/razones/registros/ajustes existentes (los ajustes de seguridad están excluidos).",
"logs": "Registros",
"logLevel": "Nivel de registro",
"logOperation": "Operación en registros",
"viewLogs": "Ver registros",
"exportLogs": "Exportar registros",
"clearLogs": "Borrar registros",
"logsCleared": "Registros borrados",
"systemLogs": "Registros del sistema (últimos 200)",
"noLogs": "Sin registros",
"logLevelUpdated": "Nivel de registro actualizado",
"readLogsFailed": "Fallo al leer los registros",
"logsExported": "Registros exportados",
"exportFailed": "Fallo al exportar",
"exportSuccess": "Exportación exitosa",
"importSuccess": "Importación exitosa, actualizando",
"importFailed": "Fallo al importar",
"clearFailed": "Fallo al borrar",
"confirmSettlement": "¿Confirmar la liquidación y reinicio?",
"settlementConfirm1": "Esto archivará los registros no liquidados en una fase y restablecerá todos los puntos de los estudiantes a cero.",
"settlementConfirm2": "La lista de estudiantes permanece sin cambios; el historial de liquidación es consultable en la página «Historial de liquidaciones».",
"settlementSuccess": "Liquidación exitosa, puntos reiniciados",
"settlementFailed": "Fallo en la liquidación",
"settle": "Liquidar",
"logLevels": {
"debug": "DEBUG (depuración)",
"info": "INFO (información)",
"warn": "WARN (advertencia)",
"error": "ERROR (error)"
}
},
"url": {
"title": "Protocolo URL (secscore://)",
"protocol": "Protocolo URL",
"description": "Puede invocar SecScore a través de una URL para realizar operaciones, por ejemplo:",
"register": "Registrar protocolo URL",
"registered": "Protocolo URL registrado",
"registerFailed": "Fallo al registrar",
"installerRequired": "Requiere la versión instalada de SecScore, puede no funcionar en modo desarrollo."
},
"mcp": {
"title": "Servicio MCP",
"description": "Gestionar el servicio HTTP MCP integrado para clientes MCP externos.",
"running": "En ejecución",
"stopped": "Detenido",
"noUrl": "Sin URL",
"host": "Host",
"port": "Puerto",
"start": "Iniciar",
"stop": "Detener",
"refresh": "Actualizar estado",
"hint": "Recomendado para vincular solo a 127.0.0.1; después del inicio, el punto final es http://host:port/mcp",
"hostRequired": "Por favor ingrese el host MCP",
"portInvalid": "Por favor ingrese un puerto válido (1-65535)",
"startSuccess": "Servidor MCP iniciado con éxito",
"startFailed": "Fallo al iniciar el servidor MCP",
"stopSuccess": "Servidor MCP detenido",
"stopFailed": "Fallo al detener el servidor MCP",
"startTimeout": "Tiempo de espera agotado al iniciar el servidor MCP, por favor inténtelo de nuevo",
"stopTimeout": "Tiempo de espera agotado al detener el servidor MCP, por favor inténtelo de nuevo"
},
"app": {
"title": "Controles de la aplicación",
"description": "Reiniciar o cerrar rápidamente la aplicación.",
"restart": "Reiniciar aplicación",
"quit": "Cerrar aplicación",
"confirmRestartTitle": "¿Confirmar el reinicio de la aplicación?",
"confirmRestartContent": "La aplicación se cerrará y reiniciará inmediatamente.",
"confirmQuitTitle": "¿Confirmar el cierre de la aplicación?",
"confirmQuitContent": "La aplicación se cerrará inmediatamente.",
"restartFailed": "Fallo al reiniciar",
"quitFailed": "Fallo al cerrar"
},
"about": {
"title": "Acerca de",
"appName": "Gestión de puntos educativos",
"version": "Versión",
"copyright": "Copyright",
"ipcStatus": "Estado IPC",
"ipcConnected": "Conectado",
"ipcDisconnected": "Desconectado (Preload fallido)",
"environment": "Entorno",
"toggleDevTools": "Activar/desactivar herramientas de desarrollo",
"license": "SecScore está bajo licencia GPL3.0",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "Inicio",
"students": "Gestión de estudiantes",
"score": "Operaciones de puntos",
"boards": "Tableros",
"leaderboard": "Clasificación",
"settlements": "Historial de liquidaciones",
"reasons": "Gestión de razones",
"autoScore": "Añadir puntos automáticamente",
"rewardExchange": "Intercambio de recompensas",
"rewardSettings": "Ajustes de recompensa",
"settings": "Configuraciones del sistema",
"remoteDb": "Base de datos remota",
"refreshStatus": "Actualizar estado",
"syncNow": "Sincronizar ahora",
"syncSuccess": "Sincronización exitosa",
"syncFailed": "Fallo en la sincronización",
"getDbStatusFailed": "Fallo al obtener el estado de la base de datos",
"notRemoteMode": "Modo base de datos remota no activado, por favor reinicie la aplicación",
"dbNotConnected": "Base de datos no conectada"
},
"auth": {
"unlock": "Desbloquear permiso",
"unlockHint": "Ingrese la contraseña de 6 dígitos: contraseña de administrador = acceso completo, contraseña de puntos = operaciones de puntos únicamente.",
"unlockButton": "Desbloquear",
"unlocked": "Permiso desbloqueado",
"logout": "Pasado a solo lectura",
"passwordPlaceholder": "ej. 123456",
"passwordError": "Contraseña incorrecta",
"enterPassword": "Ingresar contraseña",
"lock": "Bloquear"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "Color y fondo",
"gradientLabels": {
"blue": "Azul fresco",
"pink": "Rosa suave",
"cyan": "Cian",
"purple": "Violeta"
},
"gradientDirections": {
"vertical": "Vertical",
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"generate": "Generar",
"saved": "Guardado",
"saveFailed": "Fallo al guardar",
"mode": "Modo",
"myTheme": "Mi tema"
},
"permissions": {
"admin": "Permiso de administrador",
"points": "Permiso de puntos",
"view": "Solo lectura"
},
"home": {
"title": "Página de inicio de puntos de estudiantes",
"subtitle": "{{count}} estudiantes en total, haga clic en la tarjeta para realizar operaciones",
"searchPlaceholder": "Buscar nombre/pinyin...",
"sortBy": {
"alphabet": "Ordenar por nombre",
"surname": "Agrupar por apellido",
"group": "Ordenar por grupo",
"score": "Clasificación por puntos"
},
"layoutBy": {
"grouped": "Lista agrupada",
"squareGrid": "Cuadrícula cuadrada"
},
"noStudents": "Sin datos de estudiantes, por favor añada en gestión de estudiantes",
"noMatch": "Sin estudiantes coincidentes encontrados",
"clearSearch": "Borrar búsqueda",
"points": "pts",
"currentScore": "Puntos actuales",
"quickOptions": "Opciones rápidas",
"adjustPoints": "Ajustar puntos",
"customPoints": "Puntos personalizados",
"customPointsHint": "Ingrese cualquier valor en el campo de entrada",
"reason": "Razón",
"reasonPlaceholder": "Ingrese la razón de la modificación de puntos (opcional)",
"preview": "Vista previa de la modificación",
"noReason": "(Sin razón)",
"ungrouped": "No agrupado",
"category": {
"others": "Otros"
},
"scoreAdded": "{{points}} puntos añadidos a {{name}}",
"scoreDeducted": "{{points}} puntos retirados a {{name}}",
"submitFailed": "Fallo al enviar",
"pleaseSelectPoints": "Por favor seleccione o ingrese puntos",
"reasonDefault": "{{action}} {{points}} puntos",
"studentCount": "{{count}} estudiantes",
"operationTitle": "Operación de puntos: {{name}}",
"submitOperation": "Enviar operación",
"operationTitleBatch": "Operación de puntos: {{count}} seleccionados",
"undoLastAction": "Deshacer",
"undoLastHint": "Deshacer último: {{name}} {{delta}}",
"undoUnavailable": "Sin operación de puntos para deshacer",
"undoLastSuccess": "Última operación de puntos deshecha",
"multiSelect": "Selección múltiple",
"batchMode": "Modo por lotes",
"selectAll": "Seleccionar todo",
"clearSelected": "Borrar selección",
"batchOperate": "Puntos por lotes",
"selected": "Seleccionado",
"selectedCount": "{{count}} seleccionados",
"batchSuccess": "Puntos enviados para {{count}} estudiantes",
"batchPartial": "{{success}}/{{total}} estudiantes enviados con éxito",
"selectStudentFirst": "Por favor primero seleccione uno o más estudiantes",
"addPoints": "Añadir puntos",
"deductPoints": "Retirar puntos",
"pointsChange": "Modificación de puntos",
"reasonLabel": "Razón: "
},
"students": {
"title": "Gestión de estudiantes",
"importList": "Importar lista",
"addStudent": "Añadir estudiante",
"name": "Nombre",
"group": "Grupo",
"avatar": "Avatar",
"currentScore": "Puntos actuales",
"tags": "Etiquetas",
"noTags": "Sin etiquetas",
"editTags": "Editar etiquetas",
"editAvatar": "Configurar avatar",
"deleteConfirm": "¿Confirmar la eliminación de este estudiante?",
"addTitle": "Añadir estudiante",
"addConfirm": "Añadir",
"namePlaceholder": "Ingrese el nombre del estudiante",
"groupPlaceholder": "Ingrese el nombre del grupo (opcional)",
"nameRequired": "Por favor ingrese el nombre",
"nameExists": "El nombre del estudiante ya existe",
"addSuccess": "Añadido con éxito",
"addFailed": "Fallo al añadir",
"deleteSuccess": "Eliminado con éxito",
"deleteFailed": "Fallo al eliminar",
"tagSaveSuccess": "Etiquetas guardadas con éxito",
"tagSaveFailed": "Fallo al guardar etiquetas",
"importTitle": "Importar lista",
"importTextHint": "Pegue varias líneas, un nombre de estudiante por línea",
"importTextPlaceholder": "Ejemplo:\nJuan\nMaría\nPablo",
"importByText": "Importar desde texto",
"importByXlsx": "Importar via xlsx",
"importByBanyou": "Importar desde BanYou",
"xlsxPreview": "Vista previa e importación xlsx",
"file": "Archivo",
"selectNameCol": "Haga clic en el encabezado para seleccionar la columna de nombre",
"previewRows": "Vista previa de las 50 primeras líneas",
"importConfirm": "Importar",
"importComplete": "Importación terminada: {{inserted}} añadidos, {{skipped}} ignorados",
"workerNotReady": "Worker no inicializado",
"parseXlsxFailed": "Fallo al analizar xlsx",
"selectNameColFirst": "Por favor primero seleccione la « Columna de nombre »",
"noNamesFound": "Sin nombres importables encontrados en la columna seleccionada",
"importFailed": "Fallo al importar",
"banyouCookieHint": "Después de iniciar sesión en BanYou Web, copie la cookie completa y péguela a continuación.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "Por favor primero pegue la cookie",
"banyouFetch": "Obtener lista de clases",
"banyouFetchSuccess": "{{count}} clases obtenidas",
"banyouFetchFailed": "Fallo al obtener clases BanYou",
"banyouCreatedClasses": "Clases que he creado",
"banyouJoinedClasses": "Clases que he unido",
"banyouNoClasses": "Sin datos de clases por el momento"
},
"boards": {
"title": "Gestión de tableros",
"addBoard": "Añadir tablero",
"editBoard": "Editar tablero",
"deleteBoard": "Eliminar tablero",
"name": "Nombre del tablero",
"description": "Descripción",
"visibility": "Visibilidad",
"public": "Público",
"private": "Privado",
"studentsCount": "{{count}} estudiantes",
"noBoards": "Sin tableros por el momento",
"addTitle": "Añadir tablero",
"editTitle": "Editar tablero",
"namePlaceholder": "Ingrese el nombre del tablero",
"descriptionPlaceholder": "Ingrese la descripción",
"confirmDelete": "¿Confirmar la eliminación de este tablero?",
"saved": "Guardado",
"saveFailed": "Fallo al guardar",
"deleted": "Eliminado",
"deleteFailed": "Fallo al eliminar",
"boardExists": "El tablero ya existe"
},
"leaderboard": {
"title": "Clasificación",
"sortBy": "Ordenar por",
"score": "Puntos",
"name": "Nombre",
"rank": "Rango",
"student": "Estudiante",
"points": "Puntos",
"noData": "Sin datos de clasificación",
"topStudents": "Mejores estudiantes",
"rankChanged": "Rango: {{rank}}"
},
"settlements": {
"title": "Historial de liquidaciones",
"phase": "Fase",
"date": "Fecha",
"studentCount": "Número de estudiantes",
"totalPoints": "Puntos totales",
"actions": "Acciones",
"viewDetails": "Ver detalles",
"noSettlements": "Sin liquidaciones por el momento",
"phaseDetail": "Detalles de la fase {{phase}}",
"settlementDate": "Fecha de liquidación: {{date}}",
"totalStudents": "Total de estudiantes: {{count}}",
"totalPointsChange": "Modificación total de puntos: {{points}}"
},
"reasons": {
"title": "Gestión de razones",
"addReason": "Añadir razón",
"editReason": "Editar razón",
"name": "Nombre de la razón",
"points": "Puntos",
"positive": "Positivo",
"negative": "Negativo",
"noReasons": "Sin razones por el momento",
"addTitle": "Añadir razón",
"editTitle": "Editar razón",
"namePlaceholder": "Ingrese el nombre de la razón",
"pointsPlaceholder": "Ingrese los puntos",
"confirmDelete": "¿Confirmar la eliminación de esta razón?",
"saved": "Guardado",
"saveFailed": "Fallo al guardar",
"deleted": "Eliminado",
"deleteFailed": "Fallo al eliminar",
"reasonExists": "La razón ya existe"
},
"autoScore": {
"title": "Añadir puntos automáticamente",
"addRule": "Añadir regla",
"editRule": "Editar regla",
"deleteRule": "Eliminar regla",
"ruleName": "Nombre de la regla",
"trigger": "Disparador",
"action": "Acción",
"interval": "Intervalo",
"enabled": "Habilitado",
"noRules": "Sin reglas por el momento",
"addTitle": "Añadir regla",
"editTitle": "Editar regla",
"namePlaceholder": "Ingrese el nombre de la regla",
"triggerPlaceholder": "Seleccione el disparador",
"actionPlaceholder": "Seleccione la acción",
"intervalPlaceholder": "Seleccione el intervalo",
"confirmDelete": "¿Confirmar la eliminación de esta regla?",
"saved": "Guardado",
"saveFailed": "Fallo al guardar",
"deleted": "Eliminado",
"deleteFailed": "Fallo al eliminar",
"ruleExists": "La regla ya existe"
},
"rewardExchange": {
"title": "Intercambio de recompensas",
"rewards": "Recompensas",
"exchange": "Intercambiar",
"history": "Historial",
"noRewards": "Sin recompensas por el momento",
"noHistory": "Sin historial por el momento",
"rewardName": "Nombre de la recompensa",
"cost": "Costo",
"stock": "Stock",
"exchangeSuccess": "Intercambio exitoso",
"exchangeFailed": "Fallo al intercambiar",
"insufficientPoints": "Puntos insuficientes",
"outOfStock": "Agotado"
},
"rewardSettings": {
"title": "Ajustes de recompensa",
"addReward": "Añadir recompensa",
"editReward": "Editar recompensa",
"deleteReward": "Eliminar recompensa",
"name": "Nombre",
"cost": "Costo",
"stock": "Stock",
"noRewards": "Sin recompensas por el momento",
"addTitle": "Añadir recompensa",
"editTitle": "Editar recompensa",
"namePlaceholder": "Ingrese el nombre",
"costPlaceholder": "Ingrese el costo",
"stockPlaceholder": "Ingrese el stock",
"confirmDelete": "¿Confirmar la eliminación de esta recompensa?",
"saved": "Guardado",
"saveFailed": "Fallo al guardar",
"deleted": "Eliminado",
"deleteFailed": "Fallo al eliminar",
"rewardExists": "La recompensa ya existe"
},
"recovery": {
"title": "Frase de recuperación SecScore",
"saved": "He guardado",
"export": "Exportar archivo de texto",
"exportHint": "Se recomienda exportar y almacenar fuera de línea. En caso de pérdida, no podrá ser recuperado.",
"hint": "Se recomienda exportar y almacenar fuera de línea después de la exportación. En caso de pérdida, no podrá ser recuperado."
}
}
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "Suivant",
"prev": "Précédent",
"finish": "Terminer",
"cancel": "Annuler",
"save": "Enregistrer",
"loading": "Chargement...",
"confirm": "Confirmer",
"success": "Succès",
"error": "Erreur",
"delete": "Supprimer",
"edit": "Modifier",
"add": "Ajouter",
"search": "Rechercher",
"clear": "Effacer",
"close": "Fermer",
"yes": "Oui",
"no": "Non",
"submit": "Envoyer",
"reset": "Réinitialiser",
"import": "Importer",
"export": "Exporter",
"create": "Créer",
"update": "Mettre à jour",
"refresh": "Actualiser",
"view": "Voir",
"name": "Nom",
"status": "Statut",
"operation": "Opération",
"description": "Description",
"total": "Total {{count}} éléments",
"noData": "Aucune donnée",
"pleaseSelect": "Veuillez sélectionner",
"pleaseEnter": "Veuillez entrer",
"readOnly": "Mode lecture seule",
"none": "Aucun",
"more": "Plus"
},
"oobe": {
"title": "Bienvenue sur SecScore",
"subtitle": "Expert en gestion de points éducatifs",
"step": "Étape {{current}}/{{total}}",
"skip": "Passer",
"steps": {
"entry": {
"title": "Commencer",
"description": "Choisissez la procédure à suivre",
"enterOobe": "Entrer dans l'OOBE",
"connectPostgresAutoSync": "Connecter PostgreSQL avec synchronisation automatique",
"skipDirect": "Passer l'OOBE et entrer directement"
},
"postgresql": {
"title": "Connecter PostgreSQL",
"description": "Entrez la chaîne de connexion PostgreSQL. Le système se connectera et se synchronisera automatiquement.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "Se connecter et synchroniser automatiquement"
},
"language": {
"title": "Choisir la langue",
"description": "Veuillez choisir votre langue préférée"
},
"theme": {
"title": "Définir le thème",
"description": "Choisissez l'apparence de l'interface",
"mode": "Mode",
"lightMode": "Clair",
"darkMode": "Sombre",
"primaryColor": "Couleur principale",
"backgroundGradient": "Dégradé de fond"
},
"password": {
"title": "Définir le mot de passe",
"description": "Définissez un mot de passe pour protéger vos données (facultatif)",
"adminPassword": "Mot de passe administrateur",
"adminPasswordHint": "Accès à toutes les fonctionnalités, 6 chiffres",
"pointsPassword": "Mot de passe points",
"pointsPasswordHint": "Opérations sur les points uniquement, 6 chiffres",
"passwordPlaceholder": "Entrez 6 chiffres",
"hint": "Laisser vide pour ignorer, configurer plus tard dans les paramètres"
},
"students": {
"title": "Importer la liste d'élèves",
"description": "Ajoutez une liste d'élèves pour commencer la gestion des points",
"import": "Importer la liste",
"manual": "Ajout manuel",
"importHint": "Supporte les fichiers Excel (.xlsx) ou JSON",
"manualHint": "Collez plusieurs lignes, un nom d'élève par ligne",
"dragDrop": "Glissez-déposez des fichiers ici ou cliquez pour sélectionner",
"supportedFormats": "Formats supportés : .xlsx, .json",
"studentName": "Nom de l'élève",
"addStudent": "Ajouter un élève",
"noStudents": "Aucun élève pour le moment, veuillez ajouter ou importer",
"studentCount": "{{count}} élèves ajoutés",
"studentExists": "L'élève existe déjà",
"importSuccess": "{{count}} élèves importés avec succès",
"noNewStudents": "Aucun nouvel élève à importer",
"parseFailed": "Échec de l'analyse du fichier",
"unsupportedFormat": "Format de fichier non supporté"
},
"reasons": {
"title": "Définir les raisons",
"description": "Ajoutez les raisons courantes de modification de points",
"addReason": "Ajouter une raison",
"reasonName": "Nom de la raison",
"points": "Points",
"positive": "Ajouter des points",
"negative": "Retirer des points",
"noReasons": "Aucune raison pour le moment, veuillez ajouter des raisons courantes",
"reasonCount": "{{count}} raisons ajoutées",
"reasonExists": "La raison existe déjà"
},
"start": {
"title": "Prêt à l'emploi",
"description": "Tout est prêt. Commençons à utiliser SecScore !",
"features": {
"score": "Gérez facilement les points des élèves",
"history": "Historique complet des modifications de points",
"settlement": "Supporte le règlement et l'archivage"
},
"startButton": "Commencer"
}
}
},
"settings": {
"title": "Paramètres système",
"tabs": {
"appearance": "Apparence",
"security": "Sécurité",
"account": "Compte",
"database": "Connexion à la base de données",
"dataManagement": "Gestion des données",
"urlProtocol": "Protocole URL",
"about": "À propos"
},
"language": "Langue",
"languageHint": "Choisir la langue d'affichage de l'interface",
"fontFamily": "Police globale",
"fontFamilyHint": "Choisir la police de l'interface (nécessite un rafraîchissement)",
"theme": "Thème",
"password": "Définir le mot de passe",
"themeMode": "Mode",
"lightMode": "Clair",
"darkMode": "Sombre",
"primaryColor": "Couleur principale",
"backgroundGradient": "Dégradé de fond",
"searchKeyboard": {
"title": "Clavier de recherche",
"hint": "Choisir la disposition du clavier sous la zone de recherche",
"disableToggle": "Désactiver le clavier de recherche",
"disableHint": "Le clavier ne s'affichera plus sous la zone de recherche",
"options": {
"qwerty26": "26 touches (QWERTY)",
"t9": "9 touches (T9)"
}
},
"interfaceZoom": "Zoom de l'interface",
"zoomOptions": {
"small70": "Petit (70%)",
"default100": "Par défaut (100%)",
"large150": "Grand (150%)"
},
"general": {
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement"
},
"zoomHint": "Ajuster la taille globale de l'interface",
"mobile": {
"navigationTitle": "Entrées de page",
"bottomNav": {
"label": "Fonctions de navigation inférieure",
"hint": "Choisir les fonctions à afficher dans la barre inférieure du mobile. La barre affiche maximum 4 éléments, les autres vont dans « Plus »."
},
"dragHint": "Glissez pour réorganiser. Les 4 premiers éléments apparaissent dans la barre inférieure, les autres dans « Plus ».",
"dropHere": "Relâchez ici",
"bottomSection": "Barre inférieure (max 4)",
"moreSection": "Plus",
"moveToMore": "Déplacer vers Plus",
"moveToBottom": "Déplacer vers le bas"
},
"zoomUpdated": "Zoom de l'interface mis à jour",
"updateFailed": "Échec de la mise à jour",
"security": {
"title": "Système de protection par mot de passe",
"adminPassword": "Mot de passe administrateur",
"pointsPassword": "Mot de passe points",
"recoveryString": "Phrase de récupération",
"set": "Défini",
"notSet": "Non défini",
"generated": "Généré",
"notGenerated": "Non généré",
"enterPassword": "Entrez 6 chiffres (laisser vide pour ne pas modifier)",
"adminPasswordPlaceholder": "Entrez 6 chiffres pour le mot de passe administrateur (laisser vide pour ne pas modifier)",
"pointsPasswordPlaceholder": "Entrez 6 chiffres pour le mot de passe points (laisser vide pour ne pas modifier)",
"recoveryPlaceholder": "Entrez la phrase de récupération",
"savePassword": "Enregistrer le mot de passe",
"savePasswords": "Enregistrer les mots de passe",
"generateRecovery": "Générer la phrase de récupération",
"clearAllPasswords": "Effacer tous les mots de passe",
"recoveryReset": "Réinitialiser la phrase de récupération",
"enterRecovery": "Entrez la phrase de récupération",
"resetPassword": "Réinitialiser le mot de passe",
"resetHint": "La réinitialisation effacera les mots de passe administrateur et points, et générera une nouvelle phrase de récupération.",
"confirmClear": "Confirmer l'effacement de tous les mots de passe ?",
"clearHint": "Après effacement, la protection sera désactivée (aucun mot de passe = permission administrateur par défaut).",
"saved": "Mot de passe mis à jour",
"saveFailed": "Échec de la mise à jour",
"generateFailed": "Échec de la génération",
"resetFailed": "Échec de la réinitialisation",
"cleared": "Effacé",
"clearFailed": "Échec de l'effacement",
"passwordClearedNewRecovery": "Mot de passe effacé, nouvelle phrase de récupération",
"newRecoveryString": "Nouvelle phrase de récupération (veuillez l'enregistrer)"
},
"account": {
"title": "Compte SECTL Auth",
"notLoggedIn": "Non connecté à SECTL Auth",
"oauthHint": "Connectez-vous avec votre compte SECTL Auth pour profiter de l'authentification unifiée et de la déconnexion à distance",
"loginButton": "Se connecter avec SECTL Auth",
"loginSuccess": "Connexion réussie",
"logout": "Déconnexion",
"userId": "ID utilisateur",
"permission": "Niveau de permission"
},
"database": {
"title": "Connexion à la base de données",
"currentStatus": "Statut actuel de la base de données",
"sqliteLocal": "Base de données SQLite locale",
"postgresqlRemote": "Base de données PostgreSQL distante",
"connected": "Connecté",
"disconnected": "Déconnecté",
"postgresqlConnection": "Connexion PostgreSQL distante",
"connectionHint": "Entrez la chaîne de connexion PostgreSQL pour se connecter à une base de données distante, supporte la synchronisation multi-plateforme.",
"testConnection": "Tester la connexion",
"switchToPostgreSQL": "Passer à PostgreSQL",
"switchToSQLite": "Passer à SQLite local",
"connectionTestSuccess": "Test de connexion réussi",
"connectionTestFailed": "Échec du test de connexion",
"switchedTo": "Passé à la base de données {{type}}",
"switchedToSQLite": "Passé à la base de données SQLite locale",
"switchFailed": "Échec du changement",
"syncDescription": "Instructions de synchronisation multi-plateforme",
"syncPoint1": "L'utilisation d'une base de données PostgreSQL distante permet la synchronisation des données multi-plateforme.",
"syncPoint2": "Mécanisme de file d'opérations intégré pour assurer la cohérence des données lors des opérations simultanées.",
"syncPoint3": "Un redémarrage de l'application est nécessaire après le changement de base de données.",
"syncPoint4": "Services de base de données cloud recommandés : Neon, Supabase, AWS RDS, etc.",
"enterConnectionString": "Veuillez entrer la chaîne de connexion PostgreSQL",
"connectionExample": "Exemple : postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "Télécharger les données locales vers le serveur distant",
"uploadHint": "Après connexion à la base de données distante, téléchargez et fusionnez l'historique SQLite local vers le serveur distant.",
"uploadNeedRemote": "Veuillez d'abord passer et connecter à la base de données PostgreSQL distante",
"noNeedUpload": "Les données locales et distantes sont déjà cohérentes",
"uploadConfirmTitle": "Confirmer le téléchargement des données locales vers le serveur distant ?",
"uploadConfirmContent": "La synchronisation privilégiera les données locales. Détecté : local-only {{localOnly}}, distant-only {{remoteOnly}}, conflits {{conflicts}}.",
"uploadSuccess": "Données locales téléchargées vers le serveur distant",
"uploadFailed": "Échec du téléchargement"
},
"data": {
"title": "Gestion des données",
"settlement": "Règlement",
"settlementAndRestart": "Régler et redémarrer",
"settlementHint": "Classe les enregistrements non régulés dans une phase et réinitialise tous les points des élèves à zéro.",
"importExport": "Importer / Exporter",
"exportJson": "Exporter JSON",
"importJson": "Importer JSON",
"importHint": "L'importation écrasera les élèves/raisons/enregistrements/paramètres existants (les paramètres de sécurité sont exclus).",
"logs": "Journaux",
"logLevel": "Niveau de journal",
"logOperation": "Opération sur les journaux",
"viewLogs": "Voir les journaux",
"exportLogs": "Exporter les journaux",
"clearLogs": "Effacer les journaux",
"logsCleared": "Journaux effacés",
"systemLogs": "Journaux système (200 derniers)",
"noLogs": "Aucun journal",
"logLevelUpdated": "Niveau de journal mis à jour",
"readLogsFailed": "Échec de la lecture des journaux",
"logsExported": "Journaux exportés",
"exportFailed": "Échec de l'exportation",
"exportSuccess": "Exportation réussie",
"importSuccess": "Importation réussie, actualisation en cours",
"importFailed": "Échec de l'importation",
"clearFailed": "Échec de l'effacement",
"confirmSettlement": "Confirmer le règlement et le redémarrage ?",
"settlementConfirm1": "Cela archivera les enregistrements non régulés dans une phase et réinitialisera tous les points des élèves à zéro.",
"settlementConfirm2": "La liste des élèves reste inchangée ; l'historique du règlement est consultable dans la page « Historique des règlements ».",
"settlementSuccess": "Règlement réussi, points redémarrés",
"settlementFailed": "Échec du règlement",
"settle": "Régler",
"logLevels": {
"debug": "DEBUG (débogage)",
"info": "INFO (information)",
"warn": "WARN (avertissement)",
"error": "ERROR (erreur)"
}
},
"url": {
"title": "Protocole URL (secscore://)",
"protocol": "Protocole URL",
"description": "Vous pouvez invoquer SecScore via une URL pour effectuer des opérations, par exemple :",
"register": "Enregistrer le protocole URL",
"registered": "Protocole URL enregistré",
"registerFailed": "Échec de l'enregistrement",
"installerRequired": "Nécessite la version installée de SecScore, peut ne pas fonctionner en mode développement."
},
"mcp": {
"title": "Service MCP",
"description": "Gérer le service HTTP MCP intégré pour les clients MCP externes.",
"running": "En cours d'exécution",
"stopped": "Arrêté",
"noUrl": "Aucune URL",
"host": "Hôte",
"port": "Port",
"start": "Démarrer",
"stop": "Arrêter",
"refresh": "Actualiser l'état",
"hint": "Recommandé pour lier uniquement à 127.0.0.1 ; après le démarrage, le point de terminaison est http://host:port/mcp",
"hostRequired": "Veuillez entrer l'hôte MCP",
"portInvalid": "Veuillez entrer un port valide (1-65535)",
"startSuccess": "Serveur MCP démarré avec succès",
"startFailed": "Échec du démarrage du serveur MCP",
"stopSuccess": "Serveur MCP arrêté",
"stopFailed": "Échec de l'arrêt du serveur MCP",
"startTimeout": "Démarrage du serveur MCP dépassé, veuillez réessayer",
"stopTimeout": "Arrêt du serveur MCP dépassé, veuillez réessayer"
},
"app": {
"title": "Contrôles de l'application",
"description": "Redémarrer ou quitter rapidement l'application.",
"restart": "Redémarrer l'application",
"quit": "Quitter l'application",
"confirmRestartTitle": "Confirmer le redémarrage de l'application ?",
"confirmRestartContent": "L'application se fermera et redémarrera immédiatement.",
"confirmQuitTitle": "Confirmer la fermeture de l'application ?",
"confirmQuitContent": "L'application se fermera immédiatement.",
"restartFailed": "Échec du redémarrage",
"quitFailed": "Échec de la fermeture"
},
"about": {
"title": "À propos",
"appName": "Gestion des points éducatifs",
"version": "Version",
"copyright": "Copyright",
"ipcStatus": "Statut IPC",
"ipcConnected": "Connecté",
"ipcDisconnected": "Déconnecté (Preload échoué)",
"environment": "Environnement",
"toggleDevTools": "Activer/désactiver les outils de développement",
"license": "SecScore est sous licence GPL3.0",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "Accueil",
"students": "Gestion des élèves",
"score": "Opérations sur les points",
"boards": "Tableaux",
"leaderboard": "Classement",
"settlements": "Historique des règlements",
"reasons": "Gestion des raisons",
"autoScore": "Ajout automatique de points",
"rewardExchange": "Échange de récompenses",
"rewardSettings": "Paramètres de récompense",
"settings": "Paramètres système",
"remoteDb": "Base de données distante",
"refreshStatus": "Actualiser l'état",
"syncNow": "Synchroniser maintenant",
"syncSuccess": "Synchronisation réussie",
"syncFailed": "Échec de la synchronisation",
"getDbStatusFailed": "Échec de l'obtention du statut de la base de données",
"notRemoteMode": "Mode base de données distante non activé, veuillez redémarrer l'application",
"dbNotConnected": "Base de données non connectée"
},
"auth": {
"unlock": "Déverrouiller la permission",
"unlockHint": "Entrez le mot de passe à 6 chiffres : mot de passe administrateur = accès complet, mot de passe points = opérations sur les points uniquement.",
"unlockButton": "Déverrouiller",
"unlocked": "Permission déverrouillée",
"logout": "Passé en lecture seule",
"passwordPlaceholder": "ex. 123456",
"passwordError": "Mot de passe incorrect",
"enterPassword": "Entrer le mot de passe",
"lock": "Verrouiller"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "Couleur et fond",
"gradientLabels": {
"blue": "Bleu frais",
"pink": "Rose doux",
"cyan": "Cyan",
"purple": "Violet"
},
"gradientDirections": {
"vertical": "Vertical",
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"generate": "Générer",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"mode": "Mode",
"myTheme": "Mon thème"
},
"permissions": {
"admin": "Permission administrateur",
"points": "Permission points",
"view": "Lecture seule"
},
"home": {
"title": "Page d'accueil des points des élèves",
"subtitle": "{{count}} élèves au total, cliquez sur la carte pour effectuer des opérations",
"searchPlaceholder": "Rechercher nom/pinyin...",
"sortBy": {
"alphabet": "Tri par nom",
"surname": "Groupe par nom de famille",
"group": "Tri par groupe",
"score": "Classement par points"
},
"layoutBy": {
"grouped": "Liste groupée",
"squareGrid": "Grille carrée"
},
"noStudents": "Aucune donnée d'élève, veuillez ajouter dans la gestion des élèves",
"noMatch": "Aucun élève correspondant trouvé",
"clearSearch": "Effacer la recherche",
"points": "pts",
"currentScore": "Points actuels",
"quickOptions": "Options rapides",
"adjustPoints": "Ajuster les points",
"customPoints": "Points personnalisés",
"customPointsHint": "Entrez n'importe quelle valeur dans la zone de saisie",
"reason": "Raison",
"reasonPlaceholder": "Entrez la raison de la modification des points (facultatif)",
"preview": "Aperçu de la modification",
"noReason": "(Aucune raison)",
"ungrouped": "Non groupé",
"category": {
"others": "Autres"
},
"scoreAdded": "{{points}} points ajoutés à {{name}}",
"scoreDeducted": "{{points}} points retirés à {{name}}",
"submitFailed": "Échec de l'envoi",
"pleaseSelectPoints": "Veuillez sélectionner ou entrer des points",
"reasonDefault": "{{action}} {{points}} points",
"studentCount": "{{count}} élèves",
"operationTitle": "Opération sur les points : {{name}}",
"submitOperation": "Envoyer l'opération",
"operationTitleBatch": "Opération sur les points : {{count}} sélectionnés",
"undoLastAction": "Annuler",
"undoLastHint": "Annuler le dernier : {{name}} {{delta}}",
"undoUnavailable": "Aucune opération sur les points à annuler",
"undoLastSuccess": "Dernière opération sur les points annulée",
"multiSelect": "Sélection multiple",
"batchMode": "Mode batch",
"selectAll": "Tout sélectionner",
"clearSelected": "Effacer la sélection",
"batchOperate": "Points en batch",
"selected": "Sélectionné",
"selectedCount": "{{count}} sélectionnés",
"batchSuccess": "Points soumis pour {{count}} élèves",
"batchPartial": "{{success}}/{{total}} élèves soumis avec succès",
"selectStudentFirst": "Veuillez d'abord sélectionner un ou plusieurs élèves",
"addPoints": "Ajouter des points",
"deductPoints": "Retirer des points",
"pointsChange": "Modification des points",
"reasonLabel": "Raison : "
},
"students": {
"title": "Gestion des élèves",
"importList": "Importer la liste",
"addStudent": "Ajouter un élève",
"name": "Nom",
"group": "Groupe",
"avatar": "Avatar",
"currentScore": "Points actuels",
"tags": "Tags",
"noTags": "Aucun tag",
"editTags": "Modifier les tags",
"editAvatar": "Définir l'avatar",
"deleteConfirm": "Confirmer la suppression de cet élève ?",
"addTitle": "Ajouter un élève",
"addConfirm": "Ajouter",
"namePlaceholder": "Entrez le nom de l'élève",
"groupPlaceholder": "Entrez le nom du groupe (facultatif)",
"nameRequired": "Veuillez entrer le nom",
"nameExists": "Le nom de l'élève existe déjà",
"addSuccess": "Ajouté avec succès",
"addFailed": "Échec de l'ajout",
"deleteSuccess": "Supprimé avec succès",
"deleteFailed": "Échec de la suppression",
"tagSaveSuccess": "Tags enregistrés avec succès",
"tagSaveFailed": "Échec de l'enregistrement des tags",
"importTitle": "Importer la liste",
"importTextHint": "Collez plusieurs lignes, un nom d'élève par ligne",
"importTextPlaceholder": "Exemple :\nJean\nMarie\nPierre",
"importByText": "Importer depuis le texte",
"importByXlsx": "Importer via xlsx",
"importByBanyou": "Importer depuis BanYou",
"xlsxPreview": "Aperçu et import xlsx",
"file": "Fichier",
"selectNameCol": "Cliquez sur l'en-tête pour sélectionner la colonne de nom",
"previewRows": "Aperçu des 50 premières lignes",
"importConfirm": "Importer",
"importComplete": "Importation terminée : {{inserted}} ajoutés, {{skipped}} ignorés",
"workerNotReady": "Worker non initialisé",
"parseXlsxFailed": "Échec de l'analyse xlsx",
"selectNameColFirst": "Veuillez d'abord sélectionner la « Colonne de nom »",
"noNamesFound": "Aucun nom importable trouvé dans la colonne sélectionnée",
"importFailed": "Échec de l'importation",
"banyouCookieHint": "Après vous être connecté à BanYou Web, copiez le cookie complet et collez-le ci-dessous.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "Veuillez d'abord coller le cookie",
"banyouFetch": "Récupérer la liste des classes",
"banyouFetchSuccess": "{{count}} classes récupérées",
"banyouFetchFailed": "Échec de la récupération des classes BanYou",
"banyouCreatedClasses": "Classes que j'ai créées",
"banyouJoinedClasses": "Classes que j'ai rejointes",
"banyouNoClasses": "Aucune donnée de classe pour le moment"
},
"boards": {
"title": "Gestion des tableaux",
"addBoard": "Ajouter un tableau",
"editBoard": "Modifier le tableau",
"deleteBoard": "Supprimer le tableau",
"name": "Nom du tableau",
"description": "Description",
"visibility": "Visibilité",
"public": "Public",
"private": "Privé",
"studentsCount": "{{count}} élèves",
"noBoards": "Aucun tableau pour le moment",
"addTitle": "Ajouter un tableau",
"editTitle": "Modifier le tableau",
"namePlaceholder": "Entrez le nom du tableau",
"descriptionPlaceholder": "Entrez la description",
"confirmDelete": "Confirmer la suppression de ce tableau ?",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"deleted": "Supprimé",
"deleteFailed": "Échec de la suppression",
"boardExists": "Le tableau existe déjà"
},
"leaderboard": {
"title": "Classement",
"sortBy": "Trier par",
"score": "Points",
"name": "Nom",
"rank": "Rang",
"student": "Élève",
"points": "Points",
"noData": "Aucune donnée de classement",
"topStudents": "Meilleurs élèves",
"rankChanged": "Rang : {{rank}}"
},
"settlements": {
"title": "Historique des règlements",
"phase": "Phase",
"date": "Date",
"studentCount": "Nombre d'élèves",
"totalPoints": "Points totaux",
"actions": "Actions",
"viewDetails": "Voir les détails",
"noSettlements": "Aucun règlement pour le moment",
"phaseDetail": "Détails de la phase {{phase}}",
"settlementDate": "Date de règlement : {{date}}",
"totalStudents": "Total des élèves : {{count}}",
"totalPointsChange": "Modification totale des points : {{points}}"
},
"reasons": {
"title": "Gestion des raisons",
"addReason": "Ajouter une raison",
"editReason": "Modifier la raison",
"name": "Nom de la raison",
"points": "Points",
"positive": "Positif",
"negative": "Négatif",
"noReasons": "Aucune raison pour le moment",
"addTitle": "Ajouter une raison",
"editTitle": "Modifier la raison",
"namePlaceholder": "Entrez le nom de la raison",
"pointsPlaceholder": "Entrez les points",
"confirmDelete": "Confirmer la suppression de cette raison ?",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"deleted": "Supprimé",
"deleteFailed": "Échec de la suppression",
"reasonExists": "La raison existe déjà"
},
"autoScore": {
"title": "Ajout automatique de points",
"addRule": "Ajouter une règle",
"editRule": "Modifier la règle",
"deleteRule": "Supprimer la règle",
"ruleName": "Nom de la règle",
"trigger": "Déclencheur",
"action": "Action",
"interval": "Intervalle",
"enabled": "Activé",
"noRules": "Aucune règle pour le moment",
"addTitle": "Ajouter une règle",
"editTitle": "Modifier la règle",
"namePlaceholder": "Entrez le nom de la règle",
"triggerPlaceholder": "Sélectionnez le déclencheur",
"actionPlaceholder": "Sélectionnez l'action",
"intervalPlaceholder": "Sélectionnez l'intervalle",
"confirmDelete": "Confirmer la suppression de cette règle ?",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"deleted": "Supprimé",
"deleteFailed": "Échec de la suppression",
"ruleExists": "La règle existe déjà"
},
"rewardExchange": {
"title": "Échange de récompenses",
"rewards": "Récompenses",
"exchange": "Échanger",
"history": "Historique",
"noRewards": "Aucune récompense pour le moment",
"noHistory": "Aucun historique pour le moment",
"rewardName": "Nom de la récompense",
"cost": "Coût",
"stock": "Stock",
"exchangeSuccess": "Échange réussi",
"exchangeFailed": "Échec de l'échange",
"insufficientPoints": "Points insuffisants",
"outOfStock": "Rupture de stock"
},
"rewardSettings": {
"title": "Paramètres de récompense",
"addReward": "Ajouter une récompense",
"editReward": "Modifier la récompense",
"deleteReward": "Supprimer la récompense",
"name": "Nom",
"cost": "Coût",
"stock": "Stock",
"noRewards": "Aucune récompense pour le moment",
"addTitle": "Ajouter une récompense",
"editTitle": "Modifier la récompense",
"namePlaceholder": "Entrez le nom",
"costPlaceholder": "Entrez le coût",
"stockPlaceholder": "Entrez le stock",
"confirmDelete": "Confirmer la suppression de cette récompense ?",
"saved": "Enregistré",
"saveFailed": "Échec de l'enregistrement",
"deleted": "Supprimé",
"deleteFailed": "Échec de la suppression",
"rewardExists": "La récompense existe déjà"
},
"recovery": {
"title": "Phrase de récupération SecScore",
"saved": "J'ai enregistré",
"export": "Exporter le fichier texte",
"exportHint": "Il est recommandé d'exporter et de stocker hors ligne. En cas de perte, il ne pourra pas être récupéré.",
"hint": "Il est recommandé d'exporter et de stocker hors ligne après l'export. En cas de perte, il ne pourra pas être récupéré."
}
}
+905
View File
@@ -0,0 +1,905 @@
{
"common": {
"next": "次へ",
"prev": "前へ",
"finish": "完了",
"cancel": "キャンセル",
"save": "保存",
"loading": "読み込み中...",
"confirm": "確認",
"success": "成功",
"error": "エラー",
"delete": "削除",
"edit": "編集",
"add": "追加",
"search": "検索",
"clear": "クリア",
"close": "閉じる",
"yes": "はい",
"no": "いいえ",
"submit": "送信",
"reset": "リセット",
"import": "インポート",
"export": "エクスポート",
"create": "作成",
"update": "更新",
"refresh": "更新",
"view": "表示",
"name": "名前",
"status": "状態",
"operation": "操作",
"description": "説明",
"total": "{{count}} 件",
"noData": "データなし",
"pleaseSelect": "選択してください",
"pleaseEnter": "入力してください",
"readOnly": "読み取り専用モード",
"none": "なし",
"more": "もっと"
},
"oobe": {
"title": "SecScoreへようこそ",
"subtitle": "教育ポイント管理エキスパート",
"step": "ステップ {{current}}/{{total}}",
"skip": "スキップ",
"steps": {
"entry": {
"title": "開始",
"description": "次の操作を選択してください",
"enterOobe": "OOBEに入る",
"connectPostgresAutoSync": "PostgreSQLに接続して自動同期",
"skipDirect": "OOBEをスキップして直接入る"
},
"postgresql": {
"title": "PostgreSQLに接続",
"description": "PostgreSQL接続文字列を入力してください。自動接続・同期後にアプリに入ります。",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "接続・自動同期して入る"
},
"language": {
"title": "言語を選択",
"description": "お好みのインターフェース言語を選択してください"
},
"theme": {
"title": "テーマを設定",
"description": "お好みの外観を選択してください",
"mode": "モード",
"lightMode": "ライト",
"darkMode": "ダーク",
"primaryColor": "メインカラー",
"backgroundGradient": "背景グラデーション"
},
"password": {
"title": "パスワードを設定",
"description": "データを保護するログインパスワードを設定(任意)",
"adminPassword": "管理者パスワード",
"adminPasswordHint": "全機能にアクセス可能、6桁の数字",
"pointsPassword": "ポイントパスワード",
"pointsPasswordHint": "ポイント操作のみ、6桁の数字",
"passwordPlaceholder": "6桁の数字を入力",
"hint": "空欄でスキップ、後で設定で構成できます"
},
"students": {
"title": "名簿をインポート",
"description": "生徒名簿を追加してポイント管理を開始",
"import": "名簿をインポート",
"manual": "手動追加",
"importHint": "Excel (.xlsx) または JSON ファイルに対応",
"manualHint": "複数行のテキストを貼り付け、1行に1名前",
"dragDrop": "ファイルをここにドラッグ、またはクリックして選択",
"supportedFormats": ".xlsx, .json 形式に対応",
"studentName": "生徒名",
"addStudent": "生徒を追加",
"noStudents": "生徒がいません。追加またはインポートしてください",
"studentCount": "{{count}} 名の生徒を追加",
"studentExists": "生徒は既に存在します",
"importSuccess": "{{count}} 名の生徒をインポート成功",
"noNewStudents": "インポートする新規生徒はいません",
"parseFailed": "ファイル解析に失敗",
"unsupportedFormat": "サポートされていないファイル形式"
},
"reasons": {
"title": "理由を設定",
"description": "よく使うポイント変動理由を追加",
"addReason": "理由を追加",
"reasonName": "理由名",
"points": "ポイント",
"positive": "加点",
"negative": "減点",
"noReasons": "理由がありません。よく使う理由を追加してください",
"reasonCount": "{{count}} 件の理由を追加",
"reasonExists": "理由は既に存在します"
},
"start": {
"title": "準備完了",
"description": "すべて準備が整いました。SecScoreを使い始めましょう!",
"features": {
"score": "生徒のポイントを簡単管理",
"history": "完全なポイント変動履歴",
"settlement": "決済とアーカイブに対応"
},
"startButton": "始める"
}
}
},
"settings": {
"title": "設定",
"tabs": {
"appearance": "外観",
"security": "セキュリティ",
"account": "アカウント",
"database": "データベース接続",
"dataManagement": "データ管理",
"urlProtocol": "URLプロトコル",
"about": "について"
},
"language": "言語",
"languageHint": "インターフェースの表示言語を選択",
"fontFamily": "グローバルフォント",
"fontFamilyHint": "アプリのフォントを選択(完全に反映するにはページの更新が必要)",
"theme": "テーマ",
"password": "パスワードを設定",
"themeMode": "モード",
"lightMode": "ライト",
"darkMode": "ダーク",
"primaryColor": "メインカラー",
"backgroundGradient": "背景グラデーション",
"searchKeyboard": {
"title": "検索キーボード",
"hint": "検索ボックス下に表示するキーボードレイアウトを選択",
"disableToggle": "検索キーボードを無効化",
"disableHint": "有効にすると、検索ボックス下にキーボードが表示されなくなります",
"options": {
"qwerty26": "26キー (QWERTY)",
"t9": "9キー (T9)"
}
},
"interfaceZoom": "インターフェースズーム",
"zoomOptions": {
"small70": "小 (70%)",
"default100": "デフォルト (100%)",
"large150": "大 (150%)"
},
"general": {
"saved": "保存しました",
"saveFailed": "保存に失敗しました"
},
"zoomHint": "アプリのインターフェースの全体サイズを調整",
"mobile": {
"navigationTitle": "ページショートカット",
"bottomNav": {
"label": "下部ナビゲーション機能",
"hint": "モバイル下部バーに表示する機能を選択。バーは最大4項目、残りは「もっと見る」に収納。"
},
"dragHint": "ドラッグで並べ替え。最初の4項目が下部バーに表示、残りは「もっと見る」に。",
"dropHere": "ここにドロップ",
"bottomSection": "下部バー(最大4",
"moreSection": "もっと見る",
"moveToMore": "もっと見るに移動",
"moveToBottom": "下部バーに移動"
},
"zoomUpdated": "ズームが更新されました",
"updateFailed": "更新に失敗しました",
"security": {
"title": "パスワード保護システム",
"adminPassword": "管理者パスワード",
"pointsPassword": "ポイントパスワード",
"recoveryString": "復旧文字列",
"set": "設定済み",
"notSet": "未設定",
"generated": "生成済み",
"notGenerated": "未生成",
"enterPassword": "6桁の数字を入力(空欄でスキップ)",
"adminPasswordPlaceholder": "6桁の管理者パスワードを入力(空欄でスキップ)",
"pointsPasswordPlaceholder": "6桁のポイントパスワードを入力(空欄でスキップ)",
"recoveryPlaceholder": "復旧文字列を入力",
"savePassword": "パスワードを保存",
"savePasswords": "パスワードを保存",
"generateRecovery": "復旧文字列を生成",
"clearAllPasswords": "全パスワードをクリア",
"recoveryReset": "復旧文字列リセット",
"enterRecovery": "復旧文字列を入力",
"resetPassword": "パスワードをリセット",
"resetHint": "リセットすると管理者/ポイントパスワードがクリアされ、新しい復旧文字列が生成されます。",
"confirmClear": "全パスワードをクリアしますか?",
"clearHint": "クリア後、保護が無効になります(パスワードがない場合、管理者権限がデフォルト)。",
"saved": "パスワードが更新されました",
"saveFailed": "更新に失敗しました",
"generateFailed": "生成に失敗しました",
"resetFailed": "リセットに失敗しました",
"cleared": "クリアしました",
"clearFailed": "クリアに失敗しました",
"passwordClearedNewRecovery": "パスワードがクリアされ、新しい復旧文字列",
"newRecoveryString": "新しい復旧文字列(保存してください)"
},
"account": {
"title": "SECTL Auth アカウント",
"notLoggedIn": "SECTL Auth にログインしていません",
"oauthHint": "SECTL Auth アカウントでログインして、統合認証とリモートログアウトを利用",
"loginButton": "SECTL Auth でログイン",
"loginSuccess": "ログイン成功",
"logout": "ログアウト",
"userId": "ユーザーID",
"permission": "権限レベル"
},
"database": {
"title": "データベース接続",
"currentStatus": "現在のデータベース状態",
"sqliteLocal": "SQLite ローカルデータベース",
"postgresqlRemote": "PostgreSQL リモートデータベース",
"connected": "接続済み",
"disconnected": "未接続",
"postgresqlConnection": "PostgreSQL リモート接続",
"connectionHint": "PostgreSQL接続文字列を入力してリモートデータベースに接続。マルチデバイス同期に対応。",
"testConnection": "接続テスト",
"switchToPostgreSQL": "PostgreSQLに切り替え",
"switchToSQLite": "ローカルSQLiteに切り替え",
"connectionTestSuccess": "接続テスト成功",
"connectionTestFailed": "接続テスト失敗",
"switchedTo": "{{type}} データベースに切り替えました",
"switchedToSQLite": "SQLite ローカルデータベースに切り替えました",
"switchFailed": "切り替えに失敗しました",
"syncDescription": "マルチデバイス同期情報",
"syncPoint1": "PostgreSQLリモートデータベースを使用すると、マルチデバイスデータ同期が可能です。",
"syncPoint2": "内蔵の操作キューメカニズムにより、同時操作時のデータ整合性を確保。",
"syncPoint3": "データベース切り替え後、アプリの再起動が必要です。",
"syncPoint4": "推奨クラウドデータベースサービス:Neon、Supabase、AWS RDSなど。",
"enterConnectionString": "PostgreSQL接続文字列を入力してください",
"connectionExample": "例:postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "ローカルデータをリモートにアップロード",
"uploadHint": "リモートデータベース接続後、ローカルSQLiteの履歴データをリモートにアップロード・マージ。",
"uploadNeedRemote": "先にPostgreSQLリモートデータベースに接続してください",
"noNeedUpload": "ローカルとリモートのデータは既に一致しています",
"uploadConfirmTitle": "ローカルデータをリモートにアップロードしますか?",
"uploadConfirmContent": "ローカルデータを優先して同期します。ローカルのみ {{localOnly}}、リモートのみ {{remoteOnly}}、競合 {{conflicts}} を検出。",
"uploadSuccess": "ローカルデータをリモートにアップロードしました",
"uploadFailed": "アップロードに失敗しました"
},
"data": {
"title": "データ管理",
"settlement": "決済",
"settlementAndRestart": "決済して再開",
"settlementHint": "現在の未決済レコードを1つのフェーズにまとめ、全生徒のポイントをゼロにリセット。",
"importExport": "インポート / エクスポート",
"exportJson": "JSONエクスポート",
"importJson": "JSONインポート",
"importHint": "インポートは既存の生徒/理由/レコード/設定を上書きします(セキュリティ設定は除く)。",
"logs": "ログ",
"logLevel": "ログレベル",
"logOperation": "ログ操作",
"viewLogs": "ログを表示",
"exportLogs": "ログをエクスポート",
"clearLogs": "ログをクリア",
"logsCleared": "ログをクリアしました",
"systemLogs": "システムログ(最新200件)",
"noLogs": "ログなし",
"logLevelUpdated": "ログレベルが更新されました",
"readLogsFailed": "ログの読み取りに失敗しました",
"logsExported": "ログをエクスポートしました",
"exportFailed": "エクスポートに失敗しました",
"exportSuccess": "エクスポート成功",
"importSuccess": "インポート成功、更新中",
"importFailed": "インポートに失敗しました",
"clearFailed": "クリアに失敗しました",
"confirmSettlement": "決済して再開しますか?",
"settlementConfirm1": "現在の未決済レコードを1つのフェーズにアーカイブし、全生徒のポイントをゼロにリセットします。",
"settlementConfirm2": "生徒名簿は変更されません。決済履歴は「決済履歴」ページで確認できます。",
"settlementSuccess": "決済成功、ポイントを再開しました",
"settlementFailed": "決済に失敗しました",
"settle": "決済",
"logLevels": {
"debug": "DEBUG (デバッグ)",
"info": "INFO (情報)",
"warn": "WARN (警告)",
"error": "ERROR (エラー)"
}
},
"url": {
"title": "URLプロトコル (secscore://)",
"protocol": "URLプロトコル",
"description": "URLリンクからSecScoreを起動して操作を実行できます。例:",
"register": "URLプロトコルを登録",
"registered": "URLプロトコルが登録されました",
"registerFailed": "登録に失敗しました",
"installerRequired": "インストール版のSecScoreが必要です。開発モードでは動作しない場合があります。"
},
"mcp": {
"title": "MCPサービス",
"description": "内蔵MCP HTTPサービスを管理。外部MCPクライアントからの呼び出しに対応。",
"running": "実行中",
"stopped": "停止",
"noUrl": "URLなし",
"host": "ホスト",
"port": "ポート",
"start": "開始",
"stop": "停止",
"refresh": "状態を更新",
"hint": "127.0.0.1のみにバインドすることを推奨。開始後、エンドポイントは http://host:port/mcp",
"hostRequired": "MCPホストを入力してください",
"portInvalid": "有効なポートを入力してください(1-65535)",
"startSuccess": "MCPサーバーが開始しました",
"startFailed": "MCPサーバーの開始に失敗しました",
"stopSuccess": "MCPサーバーが停止しました",
"stopFailed": "MCPサーバーの停止に失敗しました",
"startTimeout": "MCPサーバーの開始がタイムアウトしました。後でもう一度お試しください",
"stopTimeout": "MCPサーバーの停止がタイムアウトしました。後でもう一度お試しください"
},
"app": {
"title": "アプリ制御",
"description": "アプリを素早く再起動または終了。",
"restart": "アプリを再起動",
"quit": "アプリを終了",
"confirmRestartTitle": "アプリを再起動しますか?",
"confirmRestartContent": "アプリはすぐに閉じて再起動します。",
"confirmQuitTitle": "アプリを終了しますか?",
"confirmQuitContent": "アプリはすぐに終了します。",
"restartFailed": "再起動に失敗しました",
"quitFailed": "終了に失敗しました"
},
"about": {
"title": "について",
"appName": "教育ポイント管理",
"version": "バージョン",
"copyright": "著作権",
"ipcStatus": "IPC状態",
"ipcConnected": "接続済み",
"ipcDisconnected": "未接続(Preload失敗)",
"environment": "環境",
"toggleDevTools": "開発者ツールを切り替え",
"license": "SecScoreはGPL3.0ライセンスに従います",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "ホーム",
"students": "生徒管理",
"score": "ポイント操作",
"boards": "ボード",
"leaderboard": "ランキング",
"settlements": "決済履歴",
"reasons": "理由管理",
"autoScore": "自動加点",
"rewardExchange": "報酬交換",
"rewardSettings": "報酬設定",
"settings": "設定",
"remoteDb": "リモートDB",
"refreshStatus": "状態を更新",
"syncNow": "今すぐ同期",
"syncSuccess": "同期成功",
"syncFailed": "同期失敗",
"getDbStatusFailed": "データベース状態の取得に失敗しました",
"notRemoteMode": "リモートデータベースモードではありません。アプリを再起動してください",
"dbNotConnected": "データベース未接続"
},
"auth": {
"unlock": "権限ロック解除",
"unlockHint": "6桁のパスワードを入力:管理者パスワード=全機能、ポイントパスワード=ポイント操作のみ。",
"unlockButton": "ロック解除",
"unlocked": "権限がロック解除されました",
"logout": "読み取り専用に切り替え",
"passwordPlaceholder": "例:123456",
"passwordError": "パスワードが間違っています",
"enterPassword": "パスワードを入力",
"lock": "ロック"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "色と背景",
"gradientLabels": {
"blue": "爽やかブルー",
"pink": "ソフトピンク",
"cyan": "シアン",
"purple": "パープル"
},
"gradientDirections": {
"vertical": "縦",
"horizontal": "横",
"diagonal": "斜め"
},
"generate": "生成",
"saved": "保存しました",
"saveFailed": "保存に失敗しました",
"mode": "モード",
"myTheme": "マイテーマ"
},
"permissions": {
"admin": "管理者権限",
"points": "ポイント権限",
"view": "読み取り専用"
},
"home": {
"title": "生徒ポイントホーム",
"subtitle": "{{count}} 名の生徒、カードをクリックしてポイント操作",
"searchPlaceholder": "名前/ピンインで検索...",
"sortBy": {
"alphabet": "名前順",
"surname": "姓グループ",
"group": "グループ順",
"score": "ポイント順"
},
"layoutBy": {
"grouped": "グループリスト",
"squareGrid": "正方形グリッド"
},
"noStudents": "生徒データがありません。生徒管理で追加してください",
"noMatch": "一致する生徒が見つかりません",
"clearSearch": "検索をクリア",
"points": "点",
"currentScore": "現在のポイント",
"quickOptions": "クイックオプション",
"adjustPoints": "ポイントを調整",
"customPoints": "カスタムポイント",
"customPointsHint": "入力ボックスで任意の値を入力できます",
"reason": "理由",
"reasonPlaceholder": "ポイント変動の理由を入力(任意)",
"preview": "変更プレビュー",
"noReason": "(理由なし)",
"ungrouped": "未グループ",
"category": {
"others": "その他"
},
"scoreAdded": "{{name}} に {{points}} 点を加点しました",
"scoreDeducted": "{{name}} から {{points}} 点を減点しました",
"submitFailed": "送信に失敗しました",
"pleaseSelectPoints": "ポイントを選択または入力してください",
"reasonDefault": "{{action}}{{points}}点",
"studentCount": "{{count}} 名",
"operationTitle": "ポイント操作:{{name}}",
"submitOperation": "操作を送信",
"operationTitleBatch": "ポイント操作:{{count}} 名選択中",
"undoLastAction": "元に戻す",
"undoLastHint": "最新の操作を元に戻す:{{name}} {{delta}}",
"undoUnavailable": "元に戻せるポイント操作がありません",
"undoLastSuccess": "最後のポイント操作を元に戻しました",
"multiSelect": "複数選択",
"batchMode": "バッチモード",
"selectAll": "全選択",
"clearSelected": "選択をクリア",
"batchOperate": "バッチポイント",
"selected": "選択済み",
"selectedCount": "{{count}} 名選択",
"batchSuccess": "{{count}} 名の生徒にポイントを送信しました",
"batchPartial": "{{success}}/{{total}} 名の生徒に送信成功",
"selectStudentFirst": "先に生徒を選択してください",
"addPoints": "加点",
"deductPoints": "減点",
"pointsChange": "ポイント変更",
"reasonLabel": "理由:"
},
"students": {
"title": "生徒管理",
"importList": "名簿をインポート",
"addStudent": "生徒を追加",
"name": "名前",
"group": "グループ",
"avatar": "アバター",
"currentScore": "現在のポイント",
"tags": "タグ",
"noTags": "タグなし",
"editTags": "タグを編集",
"editAvatar": "アバターを設定",
"deleteConfirm": "この生徒を削除しますか?",
"addTitle": "生徒を追加",
"addConfirm": "追加",
"namePlaceholder": "生徒名を入力",
"groupPlaceholder": "グループ名を入力(任意)",
"nameRequired": "名前を入力してください",
"nameExists": "生徒名は既に存在します",
"addSuccess": "追加成功",
"addFailed": "追加に失敗しました",
"deleteSuccess": "削除成功",
"deleteFailed": "削除に失敗しました",
"tagSaveSuccess": "タグを保存しました",
"tagSaveFailed": "タグの保存に失敗しました",
"importTitle": "名簿をインポート",
"importTextHint": "複数行のテキストを貼り付け、1行に1名前",
"importTextPlaceholder": "例:\\n田中\\n佐藤\\n鈴木",
"importByText": "テキストからインポート",
"importByXlsx": "xlsxからインポート",
"importByBanyou": "班優からインポート",
"xlsxPreview": "xlsxプレビューとインポート",
"file": "ファイル",
"selectNameCol": "ヘッダーをクリックして名前列を選択",
"previewRows": "最初の50行をプレビュー",
"importConfirm": "インポート",
"importComplete": "インポート完了:{{inserted}} 件追加、{{skipped}} 件スキップ",
"workerNotReady": "Workerが初期化されていません",
"parseXlsxFailed": "xlsxの解析に失敗しました",
"selectNameColFirst": "先に「名前列」を選択してください",
"noNamesFound": "選択した列にインポート可能な名前が見つかりません",
"importFailed": "インポートに失敗しました",
"banyouCookieHint": "班優ウェブ版にログイン後、完全なCookieをコピーして下に貼り付けてください。",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "先にCookieを貼り付けてください",
"banyouFetch": "クラス一覧を取得",
"banyouFetchSuccess": "{{count}} 件のクラスを取得",
"banyouFetchFailed": "班優クラスの取得に失敗しました",
"banyouCreatedClasses": "作成したクラス",
"banyouJoinedClasses": "参加したクラス",
"banyouNoClasses": "クラスデータなし",
"banyouClassTeacher": "担任:",
"banyouStudentCount": "生徒:",
"banyouPraiseCount": "表彰:",
"banyouFetchDetailFailed": "クラス詳細の取得に失敗しました",
"banyouClassDetail": "クラス詳細",
"banyouDetailEmpty": "クラス詳細データなし",
"banyouReasonList": "理由一覧",
"banyouStudentList": "生徒一覧",
"banyouTeamList": "グループ一覧",
"banyouNoTeams": "グループデータなし(teamPlanIdを入力して再度開いてみてください)",
"banyouTeamPlanSelector": "グループプラン",
"banyouLoadTeamPlan": "このプランを読み込む",
"banyouTeamPlanSource": "プランソースAPI",
"banyouSelectTeamPlanFirst": "先にグループプランを選択してください",
"banyouImportSelected": "選択項目をインポート",
"banyouNothingSelected": "インポートする項目を少なくとも1つ選択してください",
"banyouImportSummary": "班優インポート完了:生徒 +{{studentsInserted}}/{{studentsSkipped}} スキップ、理由 +{{reasonsInserted}}/{{reasonsSkipped}} スキップ、グループ {{groupsUpdated}} 更新/{{groupsSkipped}} スキップ",
"editTagTitle": "タグを編集 - {{name}}",
"editGroup": "グループを設定",
"editGroupTitle": "グループを設定 - {{name}}",
"groupSaveSuccess": "グループを保存しました",
"groupSaveFailed": "グループの保存に失敗しました",
"existingGroups": "既存のグループ(クリックで入力)",
"noExistingGroups": "利用可能なグループがありません",
"groupBoardEdit": "グループを編集",
"groupBoardTitle": "クイックグループエディタ",
"groupBoardHint": "生徒をグループ列にドラッグして保存で一括適用。",
"groupBoardNewGroupPlaceholder": "新しいグループ名を入力",
"groupBoardAddGroup": "新しいグループ",
"groupBoardGroupExists": "グループは既に存在します",
"groupBoardGroupNameRequired": "グループ名を入力してください",
"groupBoardGroupNameInvalid": "無効なグループ名",
"groupBoardEmpty": "生徒をここにドラッグ",
"groupBoardNoChanges": "グループに変更はありません",
"groupBoardSaveSuccess": "{{count}} 名の生徒のグループを更新しました",
"groupBoardSaveFailed": "保存に失敗、{{failed}} 名の生徒が更新されませんでした",
"noGroup": "未グループ",
"editAvatarTitle": "アバターを設定 - {{name}}",
"avatarUpload": "画像をアップロード",
"avatarClear": "アバターをクリア",
"noAvatar": "アバターなし",
"avatarTip": "一般的な画像形式に対応、2MB以下",
"avatarSaveSuccess": "アバターを保存しました",
"avatarSaveFailed": "アバターの保存に失敗しました",
"avatarInvalidFile": "画像ファイルを選択してください",
"avatarTooLarge": "画像は2MB以下にしてください",
"avatarReadFailed": "画像の読み取りに失敗しました"
},
"score": {
"title": "ポイント管理",
"student": "生徒",
"change": "変動",
"reason": "理由",
"time": "時間",
"undoConfirm": "このレコードを元に戻しますか?生徒のポイントがロールバックされます。",
"undo": "元に戻す",
"undoSuccess": "操作を元に戻しました",
"undoFailed": "元に戻すのに失敗しました",
"recentRecords": "最近のレコード",
"pleaseSelectStudent": "生徒を選択または検索してください",
"selectAllStudents": "全クラス",
"clearSelectedStudents": "クリア",
"selectedCount": "{{selected}} / {{total}} 名選択",
"points": "ポイント",
"addPoints": "加点",
"deductPoints": "減点",
"quickReason": "クイック理由",
"selectReason": "プリセット理由を選択",
"reasonContent": "理由内容",
"reasonPlaceholder": "手動入力またはクイック理由を選択",
"submit": "確認して送信",
"pleaseEnterInfo": "完全な情報を入力してください",
"pleaseEnterPoints": "ポイントを入力するかプリセット理由を選択してください",
"batchSuccess": "{{count}} 名の生徒にポイントを送信しました",
"batchPartial": "{{success}}/{{total}} 名の生徒に送信成功"
},
"reasons": {
"title": "理由管理",
"addReason": "プリセット理由を追加",
"category": "カテゴリ",
"content": "理由内容",
"presetPoints": "プリセットポイント",
"deleteConfirm": "この理由を削除しますか?",
"addTitle": "理由を追加",
"addConfirm": "追加",
"categoryPlaceholder": "例:学習、規律",
"contentPlaceholder": "理由を入力",
"pointsPlaceholder": "例:2 または -2",
"contentRequired": "理由内容を入力してください",
"pointsRequired": "プリセットポイントを入力してください",
"reasonExists": "このカテゴリに同じ理由が既に存在します",
"addSuccess": "追加成功",
"addFailed": "追加に失敗しました",
"deleteSuccess": "削除成功",
"deleteFailed": "削除に失敗しました",
"others": "その他"
},
"rewardExchange": {
"title": "報酬交換",
"enterMode": "交換モードに入る",
"exitMode": "交換モードを終了",
"modeHint": "報酬交換モードがアクティブです。生徒カードをクリックして利用可能な報酬を選択。",
"normalHint": "現在のカードは元のスコアを表示しています。モードに入ると交換可能ポイントが表示されます。",
"remainingRewardPoints": "残り交換ポイント",
"currentScore": "現在のスコア",
"chooseRewardTitle": "{{name}} の報酬を選択",
"currentRewardPoints": "現在の交換可能ポイント:{{points}}",
"costLabel": "消費ポイント:{{points}}",
"redeemNow": "今すぐ交換",
"noAffordableRewards": "現在交換できる報酬がありません。もっとポイントを貯めてください。",
"redeemSuccess": "{{student}} が「{{reward}}」を交換し、{{points}} ポイントを消費しました",
"redeemFailed": "交換に失敗しました",
"recordsTitle": "交換記録",
"recordStudent": "生徒",
"recordReward": "報酬",
"recordCost": "消費ポイント",
"recordTime": "交換日時"
},
"rewardSettings": {
"title": "報酬設定",
"addReward": "報酬を追加",
"addTitle": "報酬を追加",
"editTitle": "報酬を編集",
"rewardName": "報酬名",
"costPoints": "消費ポイント",
"namePlaceholder": "例:宿題免除券",
"costPlaceholder": "消費ポイントを入力",
"nameRequired": "報酬名を入力してください",
"costRequired": "正の整数のポイントを入力してください",
"createSuccess": "報酬を作成しました",
"createFailed": "報酬の作成に失敗しました",
"updateSuccess": "報酬を更新しました",
"updateFailed": "報酬の更新に失敗しました",
"deleteSuccess": "報酬を削除しました",
"deleteFailed": "報酬の削除に失敗しました",
"deleteConfirm": "報酬「{{name}}」を削除しますか?"
},
"board": {
"title": "ボード",
"editable": "編集可能",
"readonly": "読み取り専用",
"addBoard": "ボードを追加",
"removeBoard": "ボードを削除",
"removeBoardConfirm": "現在のボードを削除しますか?",
"boardConfig": "ボード設定",
"boardNamePlaceholder": "ボード名を入力",
"renameBoard": "ボード名を変更",
"newBoard": "新しいボード",
"untitledBoard": "無題ボード",
"addList": "生徒リストを追加",
"removeList": "リストを削除",
"removeListConfirm": "現在の生徒リストを削除しますか?",
"editList": "編集",
"sqlEditorTitle": "生徒リストを編集",
"splitHorizontal": "左右分割",
"splitVertical": "上下分割",
"dragHandle": "ドラッグしてレイアウトを調整",
"listNamePlaceholder": "リスト名を入力",
"newList": "新しいリスト",
"applyPreset": "プリセットを適用",
"run": "SQLを実行",
"runAll": "すべて実行",
"viewModes": {
"list": "リスト表示",
"card": "カード表示",
"grid": "グリッド表示"
},
"scoreDisplayModes": {
"total": "合計スコアを表示",
"split": "加点と減点を表示"
},
"metrics": {
"totalScore": "合計",
"addScore": "加点",
"deductScore": "減点",
"rewardPoints": "報酬",
"weekChange": "7日間",
"weekDeducted": "7日間減点",
"todayAnswered": "今日"
},
"sqlPlaceholder": "SQLを入力(単一のSELECT / WITHクエリのみ)",
"templateHint": "テンプレート変数:{{today_start}}、{{this_week_start}}、{{last_week_start}}、{{since_7d}}、{{since_30d}}、{{now}}",
"templateDescription": "SQL内でテンプレート変数を直接使用。実行時にISOタイムスタンプに自動置換(SQLiteとPostgreSQL両対応)。",
"saving": "保存中...",
"saveFailed": "ボードの保存に失敗しました",
"runFailed": "クエリの実行に失敗しました",
"keepAtLeastOneBoard": "少なくとも1つのボードを保持してください",
"keepAtLeastOneList": "少なくとも1つの生徒リストを保持してください",
"presets": {
"weekLowDeduct": {
"name": "低減点ランキング(7日間)",
"description": "過去1週間の減点 < 3 の生徒ランキング"
},
"todayActive": {
"name": "今日のアクティブ",
"description": "今日の回答回数とスコア変動でソート"
},
"rewardRanking": {
"name": "報酬ポイントランキング",
"description": "報酬ポイントと合計スコアでソート"
}
}
},
"leaderboard": {
"title": "ポイントランキング",
"rank": "順位",
"name": "名前",
"totalScore": "合計ポイント",
"todayChange": "今日の変動",
"weekChange": "今週の変動",
"monthChange": "今月の変動",
"viewHistory": "表示",
"exportXlsx": "XLSXエクスポート",
"exportSuccess": "エクスポート成功",
"queryFailed": "クエリに失敗しました",
"historyTitle": "{{name}} - 操作履歴",
"close": "閉じる",
"today": "今日",
"week": "今週",
"month": "今月",
"operationRecord": "操作記録",
"change": "変動"
},
"settlements": {
"title": "決済履歴",
"phase": "フェーズ #{{id}}",
"viewLeaderboard": "ランキングを表示",
"recordCount": "レコード数:{{count}}",
"noRecords": "決済レコードなし",
"noSettlements": "決済レコードなし",
"back": "戻る",
"leaderboardTitle": "決済ランキング",
"phaseScore": "フェーズポイント",
"queryFailed": "クエリに失敗しました",
"rank": "順位",
"name": "名前"
},
"autoScore": {
"title": "自動加点管理",
"name": "自動化名",
"namePlaceholder": "例:毎日チェックイン加点",
"nameRequired": "自動化名を入力してください",
"triggers": "トリガー",
"actions": "アクション",
"applicableStudents": "対象生徒",
"allStudents": "全生徒",
"lastExecuted": "最終実行",
"notExecuted": "未実行",
"invalidTime": "無効な時間",
"addTrigger": "ルールを追加",
"addAction": "アクションを追加",
"whenTriggered": "以下のルールがトリガーされた時",
"triggeredActions": "ルールが満たされた時にトリガーされるアクション",
"addAutomation": "自動化を追加",
"updateAutomation": "自動化を更新",
"cancelEdit": "編集をキャンセル",
"resetForm": "フォームをリセット",
"deleteConfirm": "この自動化を削除しますか?",
"adminRequired": "自動加点の表示には管理者権限が必要です",
"adminCreateRequired": "自動加点の作成・更新には管理者権限が必要です",
"adminDeleteRequired": "自動加点の削除には管理者権限が必要です",
"adminToggleRequired": "自動加点の有効化/無効化には管理者権限が必要です",
"fetchFailed": "自動化の取得に失敗しました",
"unsupportedLogic": "現在の保存はAND条件のみ対応。OR / NOT論理はバックエンド未対応",
"triggerRequired": "少なくとも1つのトリガーを追加してください",
"actionRequired": "少なくとも1つのアクションを追加してください",
"createSuccess": "自動化を作成しました",
"updateSuccess": "自動化を更新しました",
"createFailed": "自動化の作成に失敗しました",
"updateFailed": "自動化の更新に失敗しました",
"deleteSuccess": "自動化を削除しました",
"deleteFailed": "自動化の削除に失敗しました",
"enabled": "自動化が有効になりました",
"disabled": "自動化が無効になりました",
"enableFailed": "自動化の有効化に失敗しました",
"disableFailed": "自動化の無効化に失敗しました",
"noTriggerAvailable": "利用可能なトリガータイプがありません",
"noActionAvailable": "利用可能なアクションタイプがありません",
"sort": "並べ替え",
"status": "状態",
"triggerCount": "{{count}} 件のトリガー",
"actionCount": "{{count}} 件のアクション",
"studentCount": "{{count}} 名の生徒",
"studentPlaceholder": "生徒を選択または検索(空欄で全生徒)",
"triggerCondition": "トリガー条件",
"executeAction": "アクションを実行",
"addOperation": "操作を追加",
"scoreLabel": "スコア",
"tagNameLabel": "タグ名",
"operationNoteLabel": "操作メモ(任意)",
"triggerIntervalTime": "間隔時間",
"triggerStudentTag": "生徒タグ",
"actionAddScore": "加点",
"actionAddTag": "タグを追加",
"intervalAmountPlaceholder": "間隔数を入力",
"intervalUnitMonth": "ヶ月後",
"intervalUnitDay": "日後",
"intervalUnitMinute": "分後",
"minutesPlaceholder": "分数を入力",
"minutes": "分",
"tagsPlaceholder": "タグ名を入力",
"scorePlaceholder": "スコアを入力",
"tagNamePlaceholder": "タグ名を入力",
"reasonPlaceholder": "理由を入力",
"relationAnd": "かつ",
"relationOr": "または"
},
"triggers": {
"studentTag": {
"label": "生徒タグ",
"description": "生徒が特定のタグに一致した時にトリガー",
"placeholder": "タグを選択",
"required": "タグ名を入力してください"
},
"randomTime": {
"label": "ランダム時間トリガー",
"description": "指定時間範囲内でランダムにトリガー",
"required": "有効な時間範囲(分)を入力してください"
},
"intervalTime": {
"label": "間隔時間トリガー",
"description": "間隔時間に達した時にトリガー",
"placeholder": "時間間隔を入力",
"required": "有効な時間を入力してください",
"monthLabel": "月",
"weekLabel": "週",
"timeLabel": "時間"
}
},
"actions": {
"sendNotification": {
"label": "通知を送信",
"description": "生徒に通知を送信",
"placeholder": "通知内容を入力"
},
"addTag": {
"label": "タグを追加",
"description": "生徒にタグを追加",
"placeholder": "タグを入力"
},
"addScore": {
"label": "スコアを追加",
"description": "生徒にスコアを追加",
"pointsPlaceholder": "スコアを入力",
"reasonPlaceholder": "理由を入力"
}
},
"wizard": {
"welcomeTitle": "SecScore ポイント管理へようこそ",
"welcomeDesc": "SecScoreを選択いただきありがとうございます。開始前に、基本設定を1分で完了してください。",
"configComplete": "設定完了!",
"configFailed": "設定の保存に失敗しました",
"startJourney": "ポイント管理を始める"
},
"tags": {
"editTitle": "タグを編集",
"inputPlaceholder": "タグ名を入力、Enterで追加",
"selectedTags": "選択済みタグ(クリックで解除)",
"noTagsSelected": "タグ未選択",
"availableTags": "利用可能なタグ(クリックで選択)",
"noAvailableTags": "利用可能なタグなし",
"fetchFailed": "タグの取得に失敗しました",
"nameTooLong": "タグ名は30文字以内にしてください",
"addFailed": "タグの追加に失敗しました",
"deleteSuccess": "タグを削除しました",
"deleteFailed": "タグの削除に失敗しました"
},
"recovery": {
"title": "SecScore 復旧文字列",
"saved": "保存しました",
"export": "テキストファイルをエクスポート",
"exportHint": "エクスポート後にオフライン保存を推奨。紛失すると復旧できません。",
"hint": "エクスポート後にオフライン保存を推奨。紛失すると復旧できません。"
}
}
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "다음",
"prev": "이전",
"finish": "완료",
"cancel": "취소",
"save": "저장",
"loading": "로딩 중...",
"confirm": "확인",
"success": "성공",
"error": "오류",
"delete": "삭제",
"edit": "편집",
"add": "추가",
"search": "검색",
"clear": "지우기",
"close": "닫기",
"yes": "예",
"no": "아니요",
"submit": "제출",
"reset": "초기화",
"import": "가져오기",
"export": "내보내기",
"create": "생성",
"update": "업데이트",
"refresh": "새로 고침",
"view": "보기",
"name": "이름",
"status": "상태",
"operation": "작업",
"description": "설명",
"total": "총 {{count}}개 항목",
"noData": "데이터 없음",
"pleaseSelect": "선택해 주세요",
"pleaseEnter": "입력해 주세요",
"readOnly": "읽기 전용 모드",
"none": "없음",
"more": "더 보기"
},
"oobe": {
"title": "SecScore에 오신 것을 환영합니다",
"subtitle": "교육 포인트 관리 전문가",
"step": "단계 {{current}}/{{total}}",
"skip": "건너뛰기",
"steps": {
"entry": {
"title": "시작하기",
"description": "진행할 절차를 선택하세요",
"enterOobe": "OOBE에 입장",
"connectPostgresAutoSync": "PostgreSQL에 자동 동기화로 연결",
"skipDirect": "OOBE 건너뛰고 바로 입장"
},
"postgresql": {
"title": "PostgreSQL 연결",
"description": "PostgreSQL 연결 문자열을 입력하세요. 시스템이 자동으로 연결되고 동기화됩니다.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "자동 연결 및 동기화 후 입장"
},
"language": {
"title": "언어 선택",
"description": "선호하는 언어를 선택해 주세요"
},
"theme": {
"title": "테마 설정",
"description": "인터페이스 모양을 선택하세요",
"mode": "모드",
"lightMode": "라이트",
"darkMode": "다크",
"primaryColor": "주요 색상",
"backgroundGradient": "배경 그라데이션"
},
"password": {
"title": "비밀번호 설정",
"description": "데이터를 보호할 로그인 비밀번호를 설정하세요 (선택 사항)",
"adminPassword": "관리자 비밀번호",
"adminPasswordHint": "모든 기능 접근 가능, 6자리 숫자",
"pointsPassword": "포인트 비밀번호",
"pointsPasswordHint": "포인트 작업만 가능, 6자리 숫자",
"passwordPlaceholder": "6자리 숫자 입력",
"hint": "비워두면 건너뛰기, 나중에 설정에서 구성 가능"
},
"students": {
"title": "학생 목록 가져오기",
"description": "학생 목록을 추가하여 포인트 관리를 시작하세요",
"import": "목록 가져오기",
"manual": "수동 추가",
"importHint": "Excel (.xlsx) 또는 JSON 파일 지원",
"manualHint": "여러 줄을 붙여넣기, 한 줄에 하나의 학생 이름",
"dragDrop": "여기에 파일을 드래그 앤 드롭하거나 클릭하여 선택",
"supportedFormats": "지원되는 형식: .xlsx, .json",
"studentName": "학생 이름",
"addStudent": "학생 추가",
"noStudents": "학생이 없습니다. 추가하거나 가져와 주세요",
"studentCount": "{{count}}명의 학생이 추가되었습니다",
"studentExists": "학생이 이미 존재합니다",
"importSuccess": "{{count}}명의 학생을 성공적으로 가져왔습니다",
"noNewStudents": "가져올 새로운 학생이 없습니다",
"parseFailed": "파일 분석 실패",
"unsupportedFormat": "지원되지 않는 파일 형식"
},
"reasons": {
"title": "이유 설정",
"description": "일반적인 포인트 변경 이유를 추가하세요",
"addReason": "이유 추가",
"reasonName": "이유 이름",
"points": "포인트",
"positive": "포인트 추가",
"negative": "포인트 차감",
"noReasons": "이유가 없습니다. 일반적인 이유를 추가해 주세요",
"reasonCount": "{{count}}개의 이유가 추가되었습니다",
"reasonExists": "이유가 이미 존재합니다"
},
"start": {
"title": "사용 준비 완료",
"description": "모든 준비가 완료되었습니다. SecScore를 사용해 봅시다!",
"features": {
"score": "학생 포인트를 쉽게 관리하세요",
"history": "완전한 포인트 변경 이력",
"settlement": "결제 및 보관 지원"
},
"startButton": "시작하기"
}
}
},
"settings": {
"title": "시스템 설정",
"tabs": {
"appearance": "모양새",
"security": "보안",
"account": "계정",
"database": "데이터베이스 연결",
"dataManagement": "데이터 관리",
"urlProtocol": "URL 프로토콜",
"about": "정보"
},
"language": "언어",
"languageHint": "인터페이스 표시 언어 선택",
"fontFamily": "글로벌 글꼴",
"fontFamilyHint": "인터페이스 글꼴 선택 (새로 고침 필요)",
"theme": "테마",
"password": "비밀번호 설정",
"themeMode": "모드",
"lightMode": "라이트",
"darkMode": "다크",
"primaryColor": "주요 색상",
"backgroundGradient": "배경 그라데이션",
"searchKeyboard": {
"title": "검색 키보드",
"hint": "검색창 아래에 표시할 키보드 레이아웃 선택",
"disableToggle": "검색 키보드 비활성화",
"disableHint": "키보드가 검색창 아래에 표시되지 않습니다",
"options": {
"qwerty26": "26자 (QWERTY)",
"t9": "9자 (T9)"
}
},
"interfaceZoom": "인터페이스 확대/축소",
"zoomOptions": {
"small70": "작게 (70%)",
"default100": "기본 (100%)",
"large150": "크게 (150%)"
},
"general": {
"saved": "저장됨",
"saveFailed": "저장 실패"
},
"zoomHint": "인터페이스 전체 크기 조정",
"mobile": {
"navigationTitle": "페이지 진입",
"bottomNav": {
"label": "하단 네비게이션 기능",
"hint": "모바일 하단 바에 표시할 기능을 선택하세요. 바는 최대 4개 항목을 표시하며, 나머지는 «더보기»로 이동합니다."
},
"dragHint": "드래그하여 재정렬. 처음 4개 항목이 하단 바에 표시되고, 나머지는 «더보기»에 있습니다.",
"dropHere": "여기에 놓으세요",
"bottomSection": "하단 바 (최대 4개)",
"moreSection": "더보기",
"moveToMore": "더보기로 이동",
"moveToBottom": "하단으로 이동"
},
"zoomUpdated": "인터페이스 확대/축소가 업데이트되었습니다",
"updateFailed": "업데이트 실패",
"security": {
"title": "비밀번호 보호 시스템",
"adminPassword": "관리자 비밀번호",
"pointsPassword": "포인트 비밀번호",
"recoveryString": "복구 문자열",
"set": "설정됨",
"notSet": "설정되지 않음",
"generated": "생성됨",
"notGenerated": "생성되지 않음",
"enterPassword": "6자리 숫자 입력 (비워두면 수정하지 않음)",
"adminPasswordPlaceholder": "6자리 관리자 비밀번호 입력 (비워두면 수정하지 않음)",
"pointsPasswordPlaceholder": "6자리 포인트 비밀번호 입력 (비워두면 수정하지 않음)",
"recoveryPlaceholder": "복구 문자열 입력",
"savePassword": "비밀번호 저장",
"savePasswords": "비밀번호 저장",
"generateRecovery": "복구 문자열 생성",
"clearAllPasswords": "모든 비밀번호 지우기",
"recoveryReset": "복구 문자열 초기화",
"enterRecovery": "복구 문자열 입력",
"resetPassword": "비밀번호 초기화",
"resetHint": "초기화하면 관리자 및 포인트 비밀번호가 지워지고, 새로운 복구 문자열이 생성됩니다.",
"confirmClear": "모든 비밀번호를 지우시겠습니까?",
"clearHint": "지운 후 보호가 비활성화됩니다 (비밀번호 없음 = 기본 관리자 권한).",
"saved": "비밀번호가 업데이트되었습니다",
"saveFailed": "업데이트 실패",
"generateFailed": "생성 실패",
"resetFailed": "초기화 실패",
"cleared": "지워짐",
"clearFailed": "지우기 실패",
"passwordClearedNewRecovery": "비밀번호가 지워졌습니다. 새로운 복구 문자열",
"newRecoveryString": "새로운 복구 문자열 (저장해 주세요)"
},
"account": {
"title": "SECTL Auth 계정",
"notLoggedIn": "SECTL Auth에 로그인하지 않았습니다",
"oauthHint": "SECTL Auth 계정으로 로그인하여 통합 인증 및 원격 로그아웃을 사용하세요",
"loginButton": "SECTL Auth로 로그인",
"loginSuccess": "로그인 성공",
"logout": "로그아웃",
"userId": "사용자 ID",
"permission": "권한 수준"
},
"database": {
"title": "데이터베이스 연결",
"currentStatus": "현재 데이터베이스 상태",
"sqliteLocal": "SQLite 로컬 데이터베이스",
"postgresqlRemote": "PostgreSQL 원격 데이터베이스",
"connected": "연결됨",
"disconnected": "연결되지 않음",
"postgresqlConnection": "PostgreSQL 원격 연결",
"connectionHint": "PostgreSQL 연결 문자열을 입력하여 원격 데이터베이스에 연결하세요. 멀티 플랫폼 동기화를 지원합니다.",
"testConnection": "연결 테스트",
"switchToPostgreSQL": "PostgreSQL로 전환",
"switchToSQLite": "SQLite 로컬로 전환",
"connectionTestSuccess": "연결 테스트 성공",
"connectionTestFailed": "연결 테스트 실패",
"switchedTo": "{{type}} 데이터베이스로 전환되었습니다",
"switchedToSQLite": "SQLite 로컬 데이터베이스로 전환되었습니다",
"switchFailed": "전환 실패",
"syncDescription": "멀티 플랫폼 동기화 지침",
"syncPoint1": "PostgreSQL 원격 데이터베이스 사용 시 멀티 플랫폼 데이터 동기화가 가능합니다.",
"syncPoint2": "내장된 작업 큐 메커니즘으로 동시 작업 시 데이터 일관성을 보장합니다.",
"syncPoint3": "데이터베이스 전환 후 애플리케이션을 재시작해야 합니다.",
"syncPoint4": "추천 클라우드 데이터베이스 서비스: Neon, Supabase, AWS RDS 등",
"enterConnectionString": "PostgreSQL 연결 문자열을 입력해 주세요",
"connectionExample": "예: postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "로컬 데이터를 원격 서버에 업로드",
"uploadHint": "원격 데이터베이스에 연결한 후, 로컬 SQLite 이력을 원격 서버에 업로드하고 병합하세요.",
"uploadNeedRemote": "먼저 PostgreSQL 원격 데이터베이스로 전환하고 연결해 주세요",
"noNeedUpload": "로컬 및 원격 데이터가 이미 일치합니다",
"uploadConfirmTitle": "로컬 데이터를 원격 서버에 업로드하시겠습니까?",
"uploadConfirmContent": "동기화는 로컬 데이터를 우선시합니다. 감지됨: 로컬 전용 {{localOnly}}, 원격 전용 {{remoteOnly}}, 충돌 {{conflicts}}.",
"uploadSuccess": "로컬 데이터가 원격 서버에 업로드되었습니다",
"uploadFailed": "업로드 실패"
},
"data": {
"title": "데이터 관리",
"settlement": "결제",
"settlementAndRestart": "결제 및 재시작",
"settlementHint": "미결제 레코드를 한 단계로 분류하고 모든 학생 포인트를 0으로 재설정합니다.",
"importExport": "가져오기 / 내보내기",
"exportJson": "JSON 내보내기",
"importJson": "JSON 가져오기",
"importHint": "가져오기는 기존 학생/이유/레코드/설정을 덮어씁니다 (보안 설정은 제외).",
"logs": "로그",
"logLevel": "로그 수준",
"logOperation": "로그 작업",
"viewLogs": "로그 보기",
"exportLogs": "로그 내보내기",
"clearLogs": "로그 지우기",
"logsCleared": "로그가 지워졌습니다",
"systemLogs": "시스템 로그 (최근 200개)",
"noLogs": "로그 없음",
"logLevelUpdated": "로그 수준이 업데이트되었습니다",
"readLogsFailed": "로그 읽기 실패",
"logsExported": "로그가 내보내졌습니다",
"exportFailed": "내보내기 실패",
"exportSuccess": "내보내기 성공",
"importSuccess": "가져오기 성공, 새로 고치는 중",
"importFailed": "가져오기 실패",
"clearFailed": "지우기 실패",
"confirmSettlement": "결제 및 재시작을 확인하시겠습니까?",
"settlementConfirm1": "미결제 레코드를 한 단계로 보관하고 모든 학생 포인트를 0으로 재설정합니다.",
"settlementConfirm2": "학생 목록은 변경되지 않습니다. 결제 이력은 «결제 이력» 페이지에서 조회할 수 있습니다.",
"settlementSuccess": "결제 성공, 포인트가 재시작되었습니다",
"settlementFailed": "결제 실패",
"settle": "결제",
"logLevels": {
"debug": "DEBUG (디버그)",
"info": "INFO (정보)",
"warn": "WARN (경고)",
"error": "ERROR (오류)"
}
},
"url": {
"title": "URL 프로토콜 (secscore://)",
"protocol": "URL 프로토콜",
"description": "URL을 통해 SecScore를 호출하여 작업을 수행할 수 있습니다. 예:",
"register": "URL 프로토콜 등록",
"registered": "URL 프로토콜이 등록되었습니다",
"registerFailed": "등록 실패",
"installerRequired": "설치된 SecScore 버전이 필요합니다. 개발 모드에서는 작동하지 않을 수 있습니다."
},
"mcp": {
"title": "MCP 서비스",
"description": "외부 MCP 클라이언트를 위한 내장 MCP HTTP 서비스를 관리하세요.",
"running": "실행 중",
"stopped": "중지됨",
"noUrl": "URL 없음",
"host": "호스트",
"port": "포트",
"start": "시작",
"stop": "중지",
"refresh": "상태 새로 고침",
"hint": "127.0.0.1에만 바인딩하는 것이 좋습니다. 시작 후 엔드포인트는 http://host:port/mcp입니다",
"hostRequired": "MCP 호스트를 입력해 주세요",
"portInvalid": "유효한 포트를 입력해 주세요 (1-65535)",
"startSuccess": "MCP 서버가 시작되었습니다",
"startFailed": "MCP 서버 시작 실패",
"stopSuccess": "MCP 서버가 중지되었습니다",
"stopFailed": "MCP 서버 중지 실패",
"startTimeout": "MCP 서버 시작 시간 초과, 다시 시도해 주세요",
"stopTimeout": "MCP 서버 중지 시간 초과, 다시 시도해 주세요"
},
"app": {
"title": "애플리케이션 제어",
"description": "애플리케이션을 빠르게 재시작하거나 종료하세요.",
"restart": "애플리케이션 재시작",
"quit": "애플리케이션 종료",
"confirmRestartTitle": "애플리케이션을 재시작하시겠습니까?",
"confirmRestartContent": "애플리케이션이 즉시 닫히고 재시작됩니다.",
"confirmQuitTitle": "애플리케이션을 종료하시겠습니까?",
"confirmQuitContent": "애플리케이션이 즉시 종료됩니다.",
"restartFailed": "재시작 실패",
"quitFailed": "종료 실패"
},
"about": {
"title": "정보",
"appName": "교육 포인트 관리",
"version": "버전",
"copyright": "저작권",
"ipcStatus": "IPC 상태",
"ipcConnected": "연결됨",
"ipcDisconnected": "연결되지 않음 (사전 로드 실패)",
"environment": "환경",
"toggleDevTools": "개발자 도구 전환",
"license": "SecScore는 GPL3.0 라이선스를 따릅니다",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "홈",
"students": "학생 관리",
"score": "포인트 작업",
"boards": "보드",
"leaderboard": "순위표",
"settlements": "결제 이력",
"reasons": "이유 관리",
"autoScore": "자동 포인트 추가",
"rewardExchange": "보상 교환",
"rewardSettings": "보상 설정",
"settings": "시스템 설정",
"remoteDb": "원격 데이터베이스",
"refreshStatus": "상태 새로 고침",
"syncNow": "지금 동기화",
"syncSuccess": "동기화 성공",
"syncFailed": "동기화 실패",
"getDbStatusFailed": "데이터베이스 상태 가져오기 실패",
"notRemoteMode": "원격 데이터베이스 모드가 활성화되지 않았습니다. 애플리케이션을 재시작해 주세요",
"dbNotConnected": "데이터베이스가 연결되지 않았습니다"
},
"auth": {
"unlock": "권한 잠금 해제",
"unlockHint": "6자리 비밀번호를 입력하세요: 관리자 비밀번호 = 전체 접근, 포인트 비밀번호 = 포인트 작업만 가능.",
"unlockButton": "잠금 해제",
"unlocked": "권한이 잠금 해제되었습니다",
"logout": "읽기 전용으로 전환되었습니다",
"passwordPlaceholder": "예: 123456",
"passwordError": "비밀번호가 잘못되었습니다",
"enterPassword": "비밀번호 입력",
"lock": "잠그기"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "색상 및 배경",
"gradientLabels": {
"blue": "청량한 파랑",
"pink": "부드러운 핑크",
"cyan": "청록",
"purple": "보라"
},
"gradientDirections": {
"vertical": "세로",
"horizontal": "가로",
"diagonal": "대각선"
},
"generate": "생성",
"saved": "저장됨",
"saveFailed": "저장 실패",
"mode": "모드",
"myTheme": "내 테마"
},
"permissions": {
"admin": "관리자 권한",
"points": "포인트 권한",
"view": "읽기 전용"
},
"home": {
"title": "학생 포인트 홈",
"subtitle": "총 {{count}}명의 학생, 카드를 클릭하여 작업 수행",
"searchPlaceholder": "이름/병음을 검색...",
"sortBy": {
"alphabet": "이름순 정렬",
"surname": "성별 그룹",
"group": "그룹순 정렬",
"score": "포인트 순위"
},
"layoutBy": {
"grouped": "그룹화된 목록",
"squareGrid": "정사각형 그리드"
},
"noStudents": "학생 데이터가 없습니다. 학생 관리에서 추가해 주세요",
"noMatch": "일치하는 학생이 없습니다",
"clearSearch": "검색 지우기",
"points": "포인트",
"currentScore": "현재 포인트",
"quickOptions": "빠른 옵션",
"adjustPoints": "포인트 조정",
"customPoints": "사용자 정의 포인트",
"customPointsHint": "입력란에 원하는 값을 입력하세요",
"reason": "이유",
"reasonPlaceholder": "포인트 변경 이유 입력 (선택 사항)",
"preview": "변경 미리보기",
"noReason": "(이유 없음)",
"ungrouped": "그룹화되지 않음",
"category": {
"others": "기타"
},
"scoreAdded": "{{name}}에게 {{points}}포인트 추가됨",
"scoreDeducted": "{{name}}에게서 {{points}}포인트 차감됨",
"submitFailed": "제출 실패",
"pleaseSelectPoints": "포인트를 선택하거나 입력해 주세요",
"reasonDefault": "{{action}} {{points}} 포인트",
"studentCount": "{{count}}명의 학생",
"operationTitle": "포인트 작업: {{name}}",
"submitOperation": "작업 제출",
"operationTitleBatch": "포인트 작업: {{count}}개 선택됨",
"undoLastAction": "실행 취소",
"undoLastHint": "마지막 작업 실행 취소: {{name}} {{delta}}",
"undoUnavailable": "실행 취소할 포인트 작업이 없습니다",
"undoLastSuccess": "마지막 포인트 작업이 실행 취소되었습니다",
"multiSelect": "다중 선택",
"batchMode": "배치 모드",
"selectAll": "모두 선택",
"clearSelected": "선택 지우기",
"batchOperate": "배치 포인트",
"selected": "선택됨",
"selectedCount": "{{count}}개 선택됨",
"batchSuccess": "{{count}}명의 학생 포인트가 제출되었습니다",
"batchPartial": "{{success}}/{{total}}명의 학생이 성공적으로 제출되었습니다",
"selectStudentFirst": "먼저 하나 이상의 학생을 선택해 주세요",
"addPoints": "포인트 추가",
"deductPoints": "포인트 차감",
"pointsChange": "포인트 변경",
"reasonLabel": "이유: "
},
"students": {
"title": "학생 관리",
"importList": "목록 가져오기",
"addStudent": "학생 추가",
"name": "이름",
"group": "그룹",
"avatar": "아바타",
"currentScore": "현재 포인트",
"tags": "태그",
"noTags": "태그 없음",
"editTags": "태그 편집",
"editAvatar": "아바타 설정",
"deleteConfirm": "이 학생을 삭제하시겠습니까?",
"addTitle": "학생 추가",
"addConfirm": "추가",
"namePlaceholder": "학생 이름 입력",
"groupPlaceholder": "그룹 이름 입력 (선택 사항)",
"nameRequired": "이름을 입력해 주세요",
"nameExists": "학생 이름이 이미 존재합니다",
"addSuccess": "추가 성공",
"addFailed": "추가 실패",
"deleteSuccess": "삭제 성공",
"deleteFailed": "삭제 실패",
"tagSaveSuccess": "태그 저장 성공",
"tagSaveFailed": "태그 저장 실패",
"importTitle": "목록 가져오기",
"importTextHint": "여러 줄을 붙여넣기, 한 줄에 하나의 학생 이름",
"importTextPlaceholder": "예:\n홍길동\n김철수\n이영희",
"importByText": "텍스트에서 가져오기",
"importByXlsx": "xlsx로 가져오기",
"importByBanyou": "반유에서 가져오기",
"xlsxPreview": "xlsx 미리보기 및 가져오기",
"file": "파일",
"selectNameCol": "헤더를 클릭하여 이름 열 선택",
"previewRows": "처음 50줄 미리보기",
"importConfirm": "가져오기",
"importComplete": "가져오기 완료: {{inserted}}개 추가, {{skipped}}개 건너뜀",
"workerNotReady": "워커가 초기화되지 않았습니다",
"parseXlsxFailed": "xlsx 분석 실패",
"selectNameColFirst": "먼저 «이름 열»을 선택해 주세요",
"noNamesFound": "선택한 열에 가져올 수 있는 이름이 없습니다",
"importFailed": "가져오기 실패",
"banyouCookieHint": "반유 웹에 로그인한 후, 전체 쿠키를 복사하여 아래에 붙여넣어 주세요.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "먼저 쿠키를 붙여넣어 주세요",
"banyouFetch": "클래스 목록 가져오기",
"banyouFetchSuccess": "{{count}}개의 클래스를 가져왔습니다",
"banyouFetchFailed": "반유 클래스 가져오기 실패",
"banyouCreatedClasses": "내가 만든 클래스",
"banyouJoinedClasses": "내가 가입한 클래스",
"banyouNoClasses": "클래스 데이터가 없습니다"
},
"boards": {
"title": "보드 관리",
"addBoard": "보드 추가",
"editBoard": "보드 편집",
"deleteBoard": "보드 삭제",
"name": "보드 이름",
"description": "설명",
"visibility": "가시성",
"public": "공개",
"private": "비공개",
"studentsCount": "{{count}}명의 학생",
"noBoards": "보드가 없습니다",
"addTitle": "보드 추가",
"editTitle": "보드 편집",
"namePlaceholder": "보드 이름 입력",
"descriptionPlaceholder": "설명 입력",
"confirmDelete": "이 보드를 삭제하시겠습니까?",
"saved": "저장됨",
"saveFailed": "저장 실패",
"deleted": "삭제됨",
"deleteFailed": "삭제 실패",
"boardExists": "보드가 이미 존재합니다"
},
"leaderboard": {
"title": "순위표",
"sortBy": "정렬 기준",
"score": "포인트",
"name": "이름",
"rank": "순위",
"student": "학생",
"points": "포인트",
"noData": "순위 데이터가 없습니다",
"topStudents": "최고의 학생들",
"rankChanged": "순위: {{rank}}"
},
"settlements": {
"title": "결제 이력",
"phase": "단계",
"date": "날짜",
"studentCount": "학생 수",
"totalPoints": "총 포인트",
"actions": "작업",
"viewDetails": "세부 정보 보기",
"noSettlements": "결제가 없습니다",
"phaseDetail": "단계 {{phase}} 세부 정보",
"settlementDate": "결제 날짜: {{date}}",
"totalStudents": "총 학생 수: {{count}}",
"totalPointsChange": "총 포인트 변경: {{points}}"
},
"reasons": {
"title": "이유 관리",
"addReason": "이유 추가",
"editReason": "이유 편집",
"name": "이유 이름",
"points": "포인트",
"positive": "긍정",
"negative": "부정",
"noReasons": "이유가 없습니다",
"addTitle": "이유 추가",
"editTitle": "이유 편집",
"namePlaceholder": "이유 이름 입력",
"pointsPlaceholder": "포인트 입력",
"confirmDelete": "이 이유를 삭제하시겠습니까?",
"saved": "저장됨",
"saveFailed": "저장 실패",
"deleted": "삭제됨",
"deleteFailed": "삭제 실패",
"reasonExists": "이유가 이미 존재합니다"
},
"autoScore": {
"title": "자동 포인트 추가",
"addRule": "규칙 추가",
"editRule": "규칙 편집",
"deleteRule": "규칙 삭제",
"ruleName": "규칙 이름",
"trigger": "트리거",
"action": "작업",
"interval": "간격",
"enabled": "활성화",
"noRules": "규칙이 없습니다",
"addTitle": "규칙 추가",
"editTitle": "규칙 편집",
"namePlaceholder": "규칙 이름 입력",
"triggerPlaceholder": "트리거 선택",
"actionPlaceholder": "작업 선택",
"intervalPlaceholder": "간격 선택",
"confirmDelete": "이 규칙을 삭제하시겠습니까?",
"saved": "저장됨",
"saveFailed": "저장 실패",
"deleted": "삭제됨",
"deleteFailed": "삭제 실패",
"ruleExists": "규칙이 이미 존재합니다"
},
"rewardExchange": {
"title": "보상 교환",
"rewards": "보상",
"exchange": "교환",
"history": "이력",
"noRewards": "보상이 없습니다",
"noHistory": "이력이 없습니다",
"rewardName": "보상 이름",
"cost": "비용",
"stock": "재고",
"exchangeSuccess": "교환 성공",
"exchangeFailed": "교환 실패",
"insufficientPoints": "포인트 부족",
"outOfStock": "재고 없음"
},
"rewardSettings": {
"title": "보상 설정",
"addReward": "보상 추가",
"editReward": "보상 편집",
"deleteReward": "보상 삭제",
"name": "이름",
"cost": "비용",
"stock": "재고",
"noRewards": "보상이 없습니다",
"addTitle": "보상 추가",
"editTitle": "보상 편집",
"namePlaceholder": "이름 입력",
"costPlaceholder": "비용 입력",
"stockPlaceholder": "재고 입력",
"confirmDelete": "이 보상을 삭제하시겠습니까?",
"saved": "저장됨",
"saveFailed": "저장 실패",
"deleted": "삭제됨",
"deleteFailed": "삭제 실패",
"rewardExists": "보상이 이미 존재합니다"
},
"recovery": {
"title": "SecScore 복구 문자열",
"saved": "저장했습니다",
"export": "텍스트 파일 내보내기",
"exportHint": "오프라인으로 저장하는 것이 좋습니다. 분실 시 복구할 수 없습니다.",
"hint": "내보낸 후 오프라인으로 저장하는 것이 좋습니다. 분실 시 복구할 수 없습니다."
}
}
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "Próximo",
"prev": "Anterior",
"finish": "Concluir",
"cancel": "Cancelar",
"save": "Salvar",
"loading": "Carregando...",
"confirm": "Confirmar",
"success": "Sucesso",
"error": "Erro",
"delete": "Excluir",
"edit": "Editar",
"add": "Adicionar",
"search": "Buscar",
"clear": "Limpar",
"close": "Fechar",
"yes": "Sim",
"no": "Não",
"submit": "Enviar",
"reset": "Redefinir",
"import": "Importar",
"export": "Exportar",
"create": "Criar",
"update": "Atualizar",
"refresh": "Atualizar",
"view": "Ver",
"name": "Nome",
"status": "Status",
"operation": "Operação",
"description": "Descrição",
"total": "Total {{count}} itens",
"noData": "Sem dados",
"pleaseSelect": "Por favor, selecione",
"pleaseEnter": "Por favor, insira",
"readOnly": "Modo somente leitura",
"none": "Nenhum",
"more": "Mais"
},
"oobe": {
"title": "Bem-vindo ao SecScore",
"subtitle": "Especialista em Gestão de Pontos Educacionais",
"step": "Passo {{current}}/{{total}}",
"skip": "Pular",
"steps": {
"entry": {
"title": "Começar",
"description": "Escolha o procedimento a ser realizado",
"enterOobe": "Entrar no OOBE",
"connectPostgresAutoSync": "Conectar PostgreSQL com sincronização automática",
"skipDirect": "Pular OOBE e entrar diretamente"
},
"postgresql": {
"title": "Conectar PostgreSQL",
"description": "Insira a string de conexão PostgreSQL. O sistema se conectará e sincronizará automaticamente.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "Conectar e sincronizar automaticamente"
},
"language": {
"title": "Escolher idioma",
"description": "Por favor, escolha seu idioma preferido"
},
"theme": {
"title": "Configurar tema",
"description": "Escolha a aparência da interface",
"mode": "Modo",
"lightMode": "Claro",
"darkMode": "Escuro",
"primaryColor": "Cor principal",
"backgroundGradient": "Gradiente de fundo"
},
"password": {
"title": "Configurar senha",
"description": "Defina uma senha para proteger seus dados (opcional)",
"adminPassword": "Senha de administrador",
"adminPasswordHint": "Acesso a todas as funções, 6 dígitos",
"pointsPassword": "Senha de pontos",
"pointsPasswordHint": "Apenas operações de pontos, 6 dígitos",
"passwordPlaceholder": "Insira 6 dígitos",
"hint": "Deixe em branco para pular, configure posteriormente nas configurações"
},
"students": {
"title": "Importar lista de alunos",
"description": "Adicione uma lista de alunos para começar a gerenciar pontos",
"import": "Importar lista",
"manual": "Adicionar manualmente",
"importHint": "Suporta arquivos Excel (.xlsx) ou JSON",
"manualHint": "Cole várias linhas, um nome de aluno por linha",
"dragDrop": "Arraste e solte arquivos aqui ou clique para selecionar",
"supportedFormats": "Formatos suportados: .xlsx, .json",
"studentName": "Nome do aluno",
"addStudent": "Adicionar aluno",
"noStudents": "Nenhum aluno ainda, por favor adicione ou importe",
"studentCount": "{{count}} alunos adicionados",
"studentExists": "Aluno já existe",
"importSuccess": "{{count}} alunos importados com sucesso",
"noNewStudents": "Nenhum novo aluno para importar",
"parseFailed": "Falha ao analisar o arquivo",
"unsupportedFormat": "Formato de arquivo não suportado"
},
"reasons": {
"title": "Configurar motivos",
"description": "Adicione motivos comuns de alteração de pontos",
"addReason": "Adicionar motivo",
"reasonName": "Nome do motivo",
"points": "Pontos",
"positive": "Adicionar pontos",
"negative": "Remover pontos",
"noReasons": "Nenhum motivo ainda, por favor adicione motivos comuns",
"reasonCount": "{{count}} motivos adicionados",
"reasonExists": "Motivo já existe"
},
"start": {
"title": "Pronto para uso",
"description": "Tudo está pronto. Vamos começar a usar o SecScore!",
"features": {
"score": "Gerencie facilmente os pontos dos alunos",
"history": "Histórico completo de alterações de pontos",
"settlement": "Suporta liquidação e arquivamento"
},
"startButton": "Começar"
}
}
},
"settings": {
"title": "Configurações do sistema",
"tabs": {
"appearance": "Aparência",
"security": "Segurança",
"account": "Conta",
"database": "Conexão com banco de dados",
"dataManagement": "Gerenciamento de dados",
"urlProtocol": "Protocolo URL",
"about": "Sobre"
},
"language": "Idioma",
"languageHint": "Escolha o idioma de exibição da interface",
"fontFamily": "Fonte global",
"fontFamilyHint": "Escolha a fonte da interface (requer atualização)",
"theme": "Tema",
"password": "Configurar senha",
"themeMode": "Modo",
"lightMode": "Claro",
"darkMode": "Escuro",
"primaryColor": "Cor principal",
"backgroundGradient": "Gradiente de fundo",
"searchKeyboard": {
"title": "Teclado de busca",
"hint": "Escolha o layout do teclado abaixo do campo de busca",
"disableToggle": "Desativar teclado de busca",
"disableHint": "O teclado não será mais exibido abaixo do campo de busca",
"options": {
"qwerty26": "26 teclas (QWERTY)",
"t9": "9 teclas (T9)"
}
},
"interfaceZoom": "Zoom da interface",
"zoomOptions": {
"small70": "Pequeno (70%)",
"default100": "Padrão (100%)",
"large150": "Grande (150%)"
},
"general": {
"saved": "Salvo",
"saveFailed": "Falha ao salvar"
},
"zoomHint": "Ajustar o tamanho total da interface",
"mobile": {
"navigationTitle": "Entradas de página",
"bottomNav": {
"label": "Funções da barra de navegação inferior",
"hint": "Escolha as funções a serem exibidas na barra inferior do celular. A barra exibe no máximo 4 itens, os outros vão para «Mais»."
},
"dragHint": "Arraste para reorganizar. Os primeiros 4 itens aparecem na barra inferior, os outros em «Mais».",
"dropHere": "Solte aqui",
"bottomSection": "Barra inferior (máx. 4)",
"moreSection": "Mais",
"moveToMore": "Mover para Mais",
"moveToBottom": "Mover para baixo"
},
"zoomUpdated": "Zoom da interface atualizado",
"updateFailed": "Falha ao atualizar",
"security": {
"title": "Sistema de proteção por senha",
"adminPassword": "Senha de administrador",
"pointsPassword": "Senha de pontos",
"recoveryString": "Frase de recuperação",
"set": "Configurado",
"notSet": "Não configurado",
"generated": "Gerado",
"notGenerated": "Não gerado",
"enterPassword": "Insira 6 dígitos (deixe em branco para pular)",
"adminPasswordPlaceholder": "Insira 6 dígitos para a senha de administrador (deixe em branco para pular)",
"pointsPasswordPlaceholder": "Insira 6 dígitos para a senha de pontos (deixe em branco para pular)",
"recoveryPlaceholder": "Insira a frase de recuperação",
"savePassword": "Salvar senha",
"savePasswords": "Salvar senhas",
"generateRecovery": "Gerar frase de recuperação",
"clearAllPasswords": "Limpar todas as senhas",
"recoveryReset": "Redefinir frase de recuperação",
"enterRecovery": "Insira a frase de recuperação",
"resetPassword": "Redefinir senha",
"resetHint": "A redefinição limpará as senhas de administrador e pontos e gerará uma nova frase de recuperação.",
"confirmClear": "Confirmar a limpeza de todas as senhas?",
"clearHint": "Após a limpeza, a proteção será desativada (sem senha = permissão de administrador por padrão).",
"saved": "Senha atualizada",
"saveFailed": "Falha ao atualizar",
"generateFailed": "Falha ao gerar",
"resetFailed": "Falha ao redefinir",
"cleared": "Limpo",
"clearFailed": "Falha ao limpar",
"passwordClearedNewRecovery": "Senha limpa, nova frase de recuperação",
"newRecoveryString": "Nova frase de recuperação (por favor, salve)"
},
"account": {
"title": "Conta SECTL Auth",
"notLoggedIn": "Não conectado ao SECTL Auth",
"oauthHint": "Conecte-se com sua conta SECTL Auth para usar autenticação unificada e logout remoto",
"loginButton": "Conectar com SECTL Auth",
"loginSuccess": "Conexão bem-sucedida",
"logout": "Sair",
"userId": "ID do usuário",
"permission": "Nível de permissão"
},
"database": {
"title": "Conexão com banco de dados",
"currentStatus": "Status atual do banco de dados",
"sqliteLocal": "Banco de dados SQLite local",
"postgresqlRemote": "Banco de dados PostgreSQL remoto",
"connected": "Conectado",
"disconnected": "Desconectado",
"postgresqlConnection": "Conexão PostgreSQL remota",
"connectionHint": "Insira a string de conexão PostgreSQL para conectar ao banco de dados remoto, suporta sincronização multiplataforma.",
"testConnection": "Testar conexão",
"switchToPostgreSQL": "Mudar para PostgreSQL",
"switchToSQLite": "Mudar para SQLite local",
"connectionTestSuccess": "Teste de conexão bem-sucedido",
"connectionTestFailed": "Falha no teste de conexão",
"switchedTo": "Mudado para o banco de dados {{type}}",
"switchedToSQLite": "Mudado para o banco de dados SQLite local",
"switchFailed": "Falha ao mudar",
"syncDescription": "Instruções de sincronização multiplataforma",
"syncPoint1": "O uso de um banco de dados PostgreSQL remoto permite a sincronização de dados multiplataforma.",
"syncPoint2": "Mecanismo de fila de operações integrado para garantir a consistência dos dados durante operações simultâneas.",
"syncPoint3": "É necessário reiniciar o aplicativo após a mudança de banco de dados.",
"syncPoint4": "Serviços de banco de dados em nuvem recomendados: Neon, Supabase, AWS RDS, etc.",
"enterConnectionString": "Por favor, insira a string de conexão PostgreSQL",
"connectionExample": "Exemplo: postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "Carregar dados locais para o servidor remoto",
"uploadHint": "Após conectar ao banco de dados remoto, carregue e mesclie o histórico SQLite local para o servidor remoto.",
"uploadNeedRemote": "Por favor, primeiro mude e conecte ao banco de dados PostgreSQL remoto",
"noNeedUpload": "Os dados locais e remotos já são consistentes",
"uploadConfirmTitle": "Confirmar o upload dos dados locais para o servidor remoto?",
"uploadConfirmContent": "A sincronização priorizará os dados locais. Detectado: apenas local {{localOnly}}, apenas remoto {{remoteOnly}}, conflitos {{conflicts}}.",
"uploadSuccess": "Dados locais carregados para o servidor remoto",
"uploadFailed": "Falha ao carregar"
},
"data": {
"title": "Gerenciamento de dados",
"settlement": "Liquidação",
"settlementAndRestart": "Liquidar e reiniciar",
"settlementHint": "Classifica os registros não liquidados em uma fase e redefine todos os pontos dos alunos para zero.",
"importExport": "Importar / Exportar",
"exportJson": "Exportar JSON",
"importJson": "Importar JSON",
"importHint": "A importação substituirá os alunos/motivos/registros/configurações existentes (configurações de segurança excluídas).",
"logs": "Logs",
"logLevel": "Nível de log",
"logOperation": "Operação de logs",
"viewLogs": "Ver logs",
"exportLogs": "Exportar logs",
"clearLogs": "Limpar logs",
"logsCleared": "Logs limpos",
"systemLogs": "Logs do sistema (últimos 200)",
"noLogs": "Sem logs",
"logLevelUpdated": "Nível de log atualizado",
"readLogsFailed": "Falha ao ler logs",
"logsExported": "Logs exportados",
"exportFailed": "Falha ao exportar",
"exportSuccess": "Exportação bem-sucedida",
"importSuccess": "Importação bem-sucedida, atualizando",
"importFailed": "Falha ao importar",
"clearFailed": "Falha ao limpar",
"confirmSettlement": "Confirmar liquidação e reinício?",
"settlementConfirm1": "Isso arquivará os registros não liquidados em uma fase e redefinirá todos os pontos dos alunos para zero.",
"settlementConfirm2": "A lista de alunos permanece inalterada; o histórico de liquidação está disponível na página «Histórico de Liquidações».",
"settlementSuccess": "Liquidação bem-sucedida, pontos reiniciados",
"settlementFailed": "Falha na liquidação",
"settle": "Liquidar",
"logLevels": {
"debug": "DEBUG (depuração)",
"info": "INFO (informação)",
"warn": "WARN (aviso)",
"error": "ERROR (erro)"
}
},
"url": {
"title": "Protocolo URL (secscore://)",
"protocol": "Protocolo URL",
"description": "Você pode invocar o SecScore por meio de uma URL para realizar operações, por exemplo:",
"register": "Registrar protocolo URL",
"registered": "Protocolo URL registrado",
"registerFailed": "Falha ao registrar",
"installerRequired": "Requer a versão instalada do SecScore, pode não funcionar no modo de desenvolvimento."
},
"mcp": {
"title": "Serviço MCP",
"description": "Gerencie o serviço HTTP MCP integrado para clientes MCP externos.",
"running": "Em execução",
"stopped": "Parado",
"noUrl": "Sem URL",
"host": "Host",
"port": "Porta",
"start": "Iniciar",
"stop": "Parar",
"refresh": "Atualizar status",
"hint": "Recomendado para vincular apenas a 127.0.0.1; após o início, o endpoint é http://host:port/mcp",
"hostRequired": "Por favor, insira o host MCP",
"portInvalid": "Por favor, insira uma porta válida (1-65535)",
"startSuccess": "Servidor MCP iniciado",
"startFailed": "Falha ao iniciar o servidor MCP",
"stopSuccess": "Servidor MCP parado",
"stopFailed": "Falha ao parar o servidor MCP",
"startTimeout": "Tempo limite excedido ao iniciar o servidor MCP, tente novamente",
"stopTimeout": "Tempo limite excedido ao parar o servidor MCP, tente novamente"
},
"app": {
"title": "Controles do aplicativo",
"description": "Reinicie ou feche rapidamente o aplicativo.",
"restart": "Reiniciar aplicativo",
"quit": "Fechar aplicativo",
"confirmRestartTitle": "Confirmar reinício do aplicativo?",
"confirmRestartContent": "O aplicativo será fechado e reiniciado imediatamente.",
"confirmQuitTitle": "Confirmar fechamento do aplicativo?",
"confirmQuitContent": "O aplicativo será fechado imediatamente.",
"restartFailed": "Falha ao reiniciar",
"quitFailed": "Falha ao fechar"
},
"about": {
"title": "Sobre",
"appName": "Gestão de Pontos Educacionais",
"version": "Versão",
"copyright": "Direitos autorais",
"ipcStatus": "Status IPC",
"ipcConnected": "Conectado",
"ipcDisconnected": "Desconectado (Falha no pré-carregamento)",
"environment": "Ambiente",
"toggleDevTools": "Alternar ferramentas de desenvolvedor",
"license": "SecScore está sob licença GPL3.0",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "Início",
"students": "Gerenciamento de alunos",
"score": "Operações de pontos",
"boards": "Quadros",
"leaderboard": "Tabela de classificação",
"settlements": "Histórico de liquidações",
"reasons": "Gerenciamento de motivos",
"autoScore": "Adição automática de pontos",
"rewardExchange": "Troca de recompensas",
"rewardSettings": "Configurações de recompensa",
"settings": "Configurações do sistema",
"remoteDb": "Banco de dados remoto",
"refreshStatus": "Atualizar status",
"syncNow": "Sincronizar agora",
"syncSuccess": "Sincronização bem-sucedida",
"syncFailed": "Falha na sincronização",
"getDbStatusFailed": "Falha ao obter status do banco de dados",
"notRemoteMode": "Modo de banco de dados remoto não ativado, reinicie o aplicativo",
"dbNotConnected": "Banco de dados não conectado"
},
"auth": {
"unlock": "Desbloquear permissão",
"unlockHint": "Insira a senha de 6 dígitos: senha de administrador = acesso total, senha de pontos = apenas operações de pontos.",
"unlockButton": "Desbloquear",
"unlocked": "Permissão desbloqueada",
"logout": "Alternado para somente leitura",
"passwordPlaceholder": "ex. 123456",
"passwordError": "Senha incorreta",
"enterPassword": "Inserir senha",
"lock": "Bloquear"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "Cor e fundo",
"gradientLabels": {
"blue": "Azul fresco",
"pink": "Rosa suave",
"cyan": "Ciano",
"purple": "Roxo"
},
"gradientDirections": {
"vertical": "Vertical",
"horizontal": "Horizontal",
"diagonal": "Diagonal"
},
"generate": "Gerar",
"saved": "Salvo",
"saveFailed": "Falha ao salvar",
"mode": "Modo",
"myTheme": "Meu tema"
},
"permissions": {
"admin": "Permissão de administrador",
"points": "Permissão de pontos",
"view": "Somente leitura"
},
"home": {
"title": "Página inicial de pontos dos alunos",
"subtitle": "Total de {{count}} alunos, clique no cartão para realizar operações",
"searchPlaceholder": "Buscar nome/pinyin...",
"sortBy": {
"alphabet": "Ordenar por nome",
"surname": "Agrupar por sobrenome",
"group": "Ordenar por grupo",
"score": "Classificação por pontos"
},
"layoutBy": {
"grouped": "Lista agrupada",
"squareGrid": "Grade quadrada"
},
"noStudents": "Sem dados de alunos, por favor adicione em gerenciamento de alunos",
"noMatch": "Nenhum aluno correspondente encontrado",
"clearSearch": "Limpar busca",
"points": "pts",
"currentScore": "Pontos atuais",
"quickOptions": "Opções rápidas",
"adjustPoints": "Ajustar pontos",
"customPoints": "Pontos personalizados",
"customPointsHint": "Insira qualquer valor no campo de entrada",
"reason": "Motivo",
"reasonPlaceholder": "Insira o motivo da alteração de pontos (opcional)",
"preview": "Visualização da alteração",
"noReason": "(Sem motivo)",
"ungrouped": "Não agrupado",
"category": {
"others": "Outros"
},
"scoreAdded": "{{points}} pontos adicionados a {{name}}",
"scoreDeducted": "{{points}} pontos removidos de {{name}}",
"submitFailed": "Falha ao enviar",
"pleaseSelectPoints": "Por favor, selecione ou insira pontos",
"reasonDefault": "{{action}} {{points}} pontos",
"studentCount": "{{count}} alunos",
"operationTitle": "Operação de pontos: {{name}}",
"submitOperation": "Enviar operação",
"operationTitleBatch": "Operação de pontos: {{count}} selecionados",
"undoLastAction": "Desfazer",
"undoLastHint": "Desfazer último: {{name}} {{delta}}",
"undoUnavailable": "Nenhuma operação de pontos para desfazer",
"undoLastSuccess": "Última operação de pontos desfeita",
"multiSelect": "Seleção múltipla",
"batchMode": "Modo em lote",
"selectAll": "Selecionar tudo",
"clearSelected": "Limpar seleção",
"batchOperate": "Pontos em lote",
"selected": "Selecionado",
"selectedCount": "{{count}} selecionados",
"batchSuccess": "Pontos enviados para {{count}} alunos",
"batchPartial": "{{success}}/{{total}} alunos enviados com sucesso",
"selectStudentFirst": "Por favor, selecione primeiro um ou mais alunos",
"addPoints": "Adicionar pontos",
"deductPoints": "Remover pontos",
"pointsChange": "Alteração de pontos",
"reasonLabel": "Motivo: "
},
"students": {
"title": "Gerenciamento de alunos",
"importList": "Importar lista",
"addStudent": "Adicionar aluno",
"name": "Nome",
"group": "Grupo",
"avatar": "Avatar",
"currentScore": "Pontos atuais",
"tags": "Tags",
"noTags": "Sem tags",
"editTags": "Editar tags",
"editAvatar": "Configurar avatar",
"deleteConfirm": "Confirmar a exclusão deste aluno?",
"addTitle": "Adicionar aluno",
"addConfirm": "Adicionar",
"namePlaceholder": "Insira o nome do aluno",
"groupPlaceholder": "Insira o nome do grupo (opcional)",
"nameRequired": "Por favor, insira o nome",
"nameExists": "Nome do aluno já existe",
"addSuccess": "Adicionado com sucesso",
"addFailed": "Falha ao adicionar",
"deleteSuccess": "Excluído com sucesso",
"deleteFailed": "Falha ao excluir",
"tagSaveSuccess": "Tags salvas com sucesso",
"tagSaveFailed": "Falha ao salvar tags",
"importTitle": "Importar lista",
"importTextHint": "Cole várias linhas, um nome de aluno por linha",
"importTextPlaceholder": "Exemplo:\nJoão\nMaria\nPedro",
"importByText": "Importar do texto",
"importByXlsx": "Importar via xlsx",
"importByBanyou": "Importar do BanYou",
"xlsxPreview": "Visualização e importação xlsx",
"file": "Arquivo",
"selectNameCol": "Clique no cabeçalho para selecionar a coluna de nomes",
"previewRows": "Visualizar as 50 primeiras linhas",
"importConfirm": "Importar",
"importComplete": "Importação concluída: {{inserted}} adicionados, {{skipped}} ignorados",
"workerNotReady": "Worker não inicializado",
"parseXlsxFailed": "Falha ao analisar xlsx",
"selectNameColFirst": "Por favor, selecione primeiro a « Coluna de nomes »",
"noNamesFound": "Nenhum nome importável encontrado na coluna selecionada",
"importFailed": "Falha ao importar",
"banyouCookieHint": "Após fazer login no BanYou Web, copie o cookie completo e cole abaixo.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "Por favor, cole o cookie primeiro",
"banyouFetch": "Obter lista de classes",
"banyouFetchSuccess": "{{count}} classes obtidas",
"banyouFetchFailed": "Falha ao obter classes do BanYou",
"banyouCreatedClasses": "Minhas classes criadas",
"banyouJoinedClasses": "Minhas classes unidas",
"banyouNoClasses": "Sem dados de classes"
},
"boards": {
"title": "Gerenciamento de quadros",
"addBoard": "Adicionar quadro",
"editBoard": "Editar quadro",
"deleteBoard": "Excluir quadro",
"name": "Nome do quadro",
"description": "Descrição",
"visibility": "Visibilidade",
"public": "Público",
"private": "Privado",
"studentsCount": "{{count}} alunos",
"noBoards": "Sem quadros",
"addTitle": "Adicionar quadro",
"editTitle": "Editar quadro",
"namePlaceholder": "Insira o nome do quadro",
"descriptionPlaceholder": "Insira a descrição",
"confirmDelete": "Confirmar a exclusão deste quadro?",
"saved": "Salvo",
"saveFailed": "Falha ao salvar",
"deleted": "Excluído",
"deleteFailed": "Falha ao excluir",
"boardExists": "Quadro já existe"
},
"leaderboard": {
"title": "Tabela de classificação",
"sortBy": "Ordenar por",
"score": "Pontos",
"name": "Nome",
"rank": "Classificação",
"student": "Aluno",
"points": "Pontos",
"noData": "Sem dados de classificação",
"topStudents": "Melhores alunos",
"rankChanged": "Classificação: {{rank}}"
},
"settlements": {
"title": "Histórico de liquidações",
"phase": "Fase",
"date": "Data",
"studentCount": "Número de alunos",
"totalPoints": "Pontos totais",
"actions": "Ações",
"viewDetails": "Ver detalhes",
"noSettlements": "Sem liquidações",
"phaseDetail": "Detalhes da fase {{phase}}",
"settlementDate": "Data de liquidação: {{date}}",
"totalStudents": "Total de alunos: {{count}}",
"totalPointsChange": "Alteração total de pontos: {{points}}"
},
"reasons": {
"title": "Gerenciamento de motivos",
"addReason": "Adicionar motivo",
"editReason": "Editar motivo",
"name": "Nome do motivo",
"points": "Pontos",
"positive": "Positivo",
"negative": "Negativo",
"noReasons": "Sem motivos",
"addTitle": "Adicionar motivo",
"editTitle": "Editar motivo",
"namePlaceholder": "Insira o nome do motivo",
"pointsPlaceholder": "Insira os pontos",
"confirmDelete": "Confirmar a exclusão deste motivo?",
"saved": "Salvo",
"saveFailed": "Falha ao salvar",
"deleted": "Excluído",
"deleteFailed": "Falha ao excluir",
"reasonExists": "Motivo já existe"
},
"autoScore": {
"title": "Adição automática de pontos",
"addRule": "Adicionar regra",
"editRule": "Editar regra",
"deleteRule": "Excluir regra",
"ruleName": "Nome da regra",
"trigger": "Gatilho",
"action": "Ação",
"interval": "Intervalo",
"enabled": "Habilitado",
"noRules": "Sem regras",
"addTitle": "Adicionar regra",
"editTitle": "Editar regra",
"namePlaceholder": "Insira o nome da regra",
"triggerPlaceholder": "Selecionar gatilho",
"actionPlaceholder": "Selecionar ação",
"intervalPlaceholder": "Selecionar intervalo",
"confirmDelete": "Confirmar a exclusão desta regra?",
"saved": "Salvo",
"saveFailed": "Falha ao salvar",
"deleted": "Excluído",
"deleteFailed": "Falha ao excluir",
"ruleExists": "Regra já existe"
},
"rewardExchange": {
"title": "Troca de recompensas",
"rewards": "Recompensas",
"exchange": "Trocar",
"history": "Histórico",
"noRewards": "Sem recompensas",
"noHistory": "Sem histórico",
"rewardName": "Nome da recompensa",
"cost": "Custo",
"stock": "Estoque",
"exchangeSuccess": "Troca bem-sucedida",
"exchangeFailed": "Falha ao trocar",
"insufficientPoints": "Pontos insuficientes",
"outOfStock": "Fora de estoque"
},
"rewardSettings": {
"title": "Configurações de recompensa",
"addReward": "Adicionar recompensa",
"editReward": "Editar recompensa",
"deleteReward": "Excluir recompensa",
"name": "Nome",
"cost": "Custo",
"stock": "Estoque",
"noRewards": "Sem recompensas",
"addTitle": "Adicionar recompensa",
"editTitle": "Editar recompensa",
"namePlaceholder": "Insira o nome",
"costPlaceholder": "Insira o custo",
"stockPlaceholder": "Insira o estoque",
"confirmDelete": "Confirmar a exclusão desta recompensa?",
"saved": "Salvo",
"saveFailed": "Falha ao salvar",
"deleted": "Excluído",
"deleteFailed": "Falha ao excluir",
"rewardExists": "Recompensa já existe"
},
"recovery": {
"title": "Frase de recuperação SecScore",
"saved": "Eu salvei",
"export": "Exportar arquivo de texto",
"exportHint": "Recomenda-se salvar offline. Se perdido, não poderá ser recuperado.",
"hint": "Recomenda-se salvar offline após a exportação. Se perdido, não poderá ser recuperado."
}
}
+668
View File
@@ -0,0 +1,668 @@
{
"common": {
"next": "Далее",
"prev": "Назад",
"finish": "Завершить",
"cancel": "Отмена",
"save": "Сохранить",
"loading": "Загрузка...",
"confirm": "Подтвердить",
"success": "Успех",
"error": "Ошибка",
"delete": "Удалить",
"edit": "Редактировать",
"add": "Добавить",
"search": "Поиск",
"clear": "Очистить",
"close": "Закрыть",
"yes": "Да",
"no": "Нет",
"submit": "Отправить",
"reset": "Сбросить",
"import": "Импорт",
"export": "Экспорт",
"create": "Создать",
"update": "Обновить",
"refresh": "Обновить",
"view": "Просмотр",
"name": "Имя",
"status": "Статус",
"operation": "Операция",
"description": "Описание",
"total": "Всего {{count}} элементов",
"noData": "Нет данных",
"pleaseSelect": "Пожалуйста, выберите",
"pleaseEnter": "Пожалуйста, введите",
"readOnly": "Режим только для чтения",
"none": "Нет",
"more": "Больше"
},
"oobe": {
"title": "Добро пожаловать в SecScore",
"subtitle": "Эксперт по управлению образовательными баллами",
"step": "Шаг {{current}}/{{total}}",
"skip": "Пропустить",
"steps": {
"entry": {
"title": "Начало работы",
"description": "Выберите процедуру, которую нужно выполнить",
"enterOobe": "Войти в OOBE",
"connectPostgresAutoSync": "Подключиться к PostgreSQL с автоматической синхронизацией",
"skipDirect": "Пропустить OOBE и войти напрямую"
},
"postgresql": {
"title": "Подключение к PostgreSQL",
"description": "Введите строку подключения к PostgreSQL. Система автоматически подключится и синхронизируется.",
"connectionPlaceholder": "postgresql://user:password@host:port/database?sslmode=require",
"autoSyncAndEnter": "Автоматическое подключение и синхронизация"
},
"language": {
"title": "Выбор языка",
"description": "Пожалуйста, выберите предпочитаемый язык"
},
"theme": {
"title": "Настройка темы",
"description": "Выберите внешний вид интерфейса",
"mode": "Режим",
"lightMode": "Светлый",
"darkMode": "Темный",
"primaryColor": "Основной цвет",
"backgroundGradient": "Градиент фона"
},
"password": {
"title": "Настройка пароля",
"description": "Установите пароль для защиты данных (необязательно)",
"adminPassword": "Пароль администратора",
"adminPasswordHint": "Доступ ко всем функциям, 6 цифр",
"pointsPassword": "Пароль баллов",
"pointsPasswordHint": "Только операции с баллами, 6 цифр",
"passwordPlaceholder": "Введите 6 цифр",
"hint": "Оставьте пустым для пропуска, настройте позже в настройках"
},
"students": {
"title": "Импорт списка студентов",
"description": "Добавьте список студентов, чтобы начать управление баллами",
"import": "Импорт списка",
"manual": "Добавить вручную",
"importHint": "Поддерживает файлы Excel (.xlsx) или JSON",
"manualHint": "Вставьте несколько строк, одно имя студента на строку",
"dragDrop": "Перетащите файлы сюда или нажмите для выбора",
"supportedFormats": "Поддерживаемые форматы: .xlsx, .json",
"studentName": "Имя студента",
"addStudent": "Добавить студента",
"noStudents": "Студентов пока нет, пожалуйста, добавьте или импортируйте",
"studentCount": "Добавлено {{count}} студентов",
"studentExists": "Студент уже существует",
"importSuccess": "Успешно импортировано {{count}} студентов",
"noNewStudents": "Нет новых студентов для импорта",
"parseFailed": "Ошибка анализа файла",
"unsupportedFormat": "Неподдерживаемый формат файла"
},
"reasons": {
"title": "Настройка причин",
"description": "Добавьте распространенные причины изменения баллов",
"addReason": "Добавить причину",
"reasonName": "Название причины",
"points": "Баллы",
"positive": "Добавить баллы",
"negative": "Снять баллы",
"noReasons": "Причин пока нет, пожалуйста, добавьте распространенные причины",
"reasonCount": "Добавлено {{count}} причин",
"reasonExists": "Причина уже существует"
},
"start": {
"title": "Готово к использованию",
"description": "Все готово. Давайте начнем использовать SecScore!",
"features": {
"score": "Легко управляйте баллами студентов",
"history": "Полная история изменений баллов",
"settlement": "Поддержка расчета и архивирования"
},
"startButton": "Начать"
}
}
},
"settings": {
"title": "Системные настройки",
"tabs": {
"appearance": "Внешний вид",
"security": "Безопасность",
"account": "Учетная запись",
"database": "Подключение к базе данных",
"dataManagement": "Управление данными",
"urlProtocol": "Протокол URL",
"about": "О программе"
},
"language": "Язык",
"languageHint": "Выберите язык отображения интерфейса",
"fontFamily": "Глобальный шрифт",
"fontFamilyHint": "Выберите шрифт интерфейса (требуется обновление)",
"theme": "Тема",
"password": "Настройка пароля",
"themeMode": "Режим",
"lightMode": "Светлый",
"darkMode": "Темный",
"primaryColor": "Основной цвет",
"backgroundGradient": "Градиент фона",
"searchKeyboard": {
"title": "Клавиатура поиска",
"hint": "Выберите раскладку клавиатуры под полем поиска",
"disableToggle": "Отключить клавиатуру поиска",
"disableHint": "Клавиатура больше не будет отображаться под полем поиска",
"options": {
"qwerty26": "26 клавиш (QWERTY)",
"t9": "9 клавиш (T9)"
}
},
"interfaceZoom": "Масштаб интерфейса",
"zoomOptions": {
"small70": "Маленький (70%)",
"default100": "По умолчанию (100%)",
"large150": "Большой (150%)"
},
"general": {
"saved": "Сохранено",
"saveFailed": "Ошибка сохранения"
},
"zoomHint": "Настройка общего размера интерфейса",
"mobile": {
"navigationTitle": "Входы на страницу",
"bottomNav": {
"label": "Функции нижней навигации",
"hint": "Выберите функции для отображения в нижней панели мобильного устройства. Панель отображает максимум 4 элемента, остальные переходят в «Ещё»."
},
"dragHint": "Перетащите для изменения порядка. Первые 4 элемента отображаются в нижней панели, остальные в «Ещё».",
"dropHere": "Отпустите здесь",
"bottomSection": "Нижняя панель (макс. 4)",
"moreSection": "Ещё",
"moveToMore": "Переместить в Ещё",
"moveToBottom": "Переместить вниз"
},
"zoomUpdated": "Масштаб интерфейса обновлен",
"updateFailed": "Ошибка обновления",
"security": {
"title": "Система защиты паролем",
"adminPassword": "Пароль администратора",
"pointsPassword": "Пароль баллов",
"recoveryString": "Строка восстановления",
"set": "Установлено",
"notSet": "Не установлено",
"generated": "Создано",
"notGenerated": "Не создано",
"enterPassword": "Введите 6 цифр (оставьте пустым для пропуска)",
"adminPasswordPlaceholder": "Введите 6 цифр пароля администратора (оставьте пустым для пропуска)",
"pointsPasswordPlaceholder": "Введите 6 цифр пароля баллов (оставьте пустым для пропуска)",
"recoveryPlaceholder": "Введите строку восстановления",
"savePassword": "Сохранить пароль",
"savePasswords": "Сохранить пароли",
"generateRecovery": "Создать строку восстановления",
"clearAllPasswords": "Очистить все пароли",
"recoveryReset": "Сброс строки восстановления",
"enterRecovery": "Введите строку восстановления",
"resetPassword": "Сброс пароля",
"resetHint": "Сброс очистит пароли администратора и баллов и создаст новую строку восстановления.",
"confirmClear": "Подтвердить очистку всех паролей?",
"clearHint": "После очистки защита будет отключена (без пароля по умолчанию - права администратора).",
"saved": "Пароль обновлен",
"saveFailed": "Ошибка обновления",
"generateFailed": "Ошибка создания",
"resetFailed": "Ошибка сброса",
"cleared": "Очищено",
"clearFailed": "Ошибка очистки",
"passwordClearedNewRecovery": "Пароль очищен, новая строка восстановления",
"newRecoveryString": "Новая строка восстановления (пожалуйста, сохраните)"
},
"account": {
"title": "Учетная запись SECTL Auth",
"notLoggedIn": "Не вошел в SECTL Auth",
"oauthHint": "Войдите в свою учетную запись SECTL Auth для использования единой аутентификации и удаленного выхода",
"loginButton": "Войти через SECTL Auth",
"loginSuccess": "Успешный вход",
"logout": "Выход",
"userId": "ID пользователя",
"permission": "Уровень разрешений"
},
"database": {
"title": "Подключение к базе данных",
"currentStatus": "Текущий статус базы данных",
"sqliteLocal": "Локальная база данных SQLite",
"postgresqlRemote": "Удаленная база данных PostgreSQL",
"connected": "Подключено",
"disconnected": "Отключено",
"postgresqlConnection": "Удаленное подключение PostgreSQL",
"connectionHint": "Введите строку подключения PostgreSQL для подключения к удаленной базе данных, поддержка синхронизации на нескольких платформах.",
"testConnection": "Тест подключения",
"switchToPostgreSQL": "Переключиться на PostgreSQL",
"switchToSQLite": "Переключиться на локальный SQLite",
"connectionTestSuccess": "Тест подключения успешен",
"connectionTestFailed": "Ошибка теста подключения",
"switchedTo": "Переключено на базу данных {{type}}",
"switchedToSQLite": "Переключено на локальную базу данных SQLite",
"switchFailed": "Ошибка переключения",
"syncDescription": "Инструкции по синхронизации на нескольких платформах",
"syncPoint1": "Использование удаленной базы данных PostgreSQL позволяет синхронизировать данные на нескольких платформах.",
"syncPoint2": "Встроенная очередь операций для обеспечения согласованности данных при одновременных операциях.",
"syncPoint3": "Требуется перезапуск приложения после переключения базы данных.",
"syncPoint4": "Рекомендуемые облачные сервисы баз данных: Neon, Supabase, AWS RDS и др.",
"enterConnectionString": "Пожалуйста, введите строку подключения PostgreSQL",
"connectionExample": "Пример: postgresql://xxxxxx_xxxxx:xxxxxxxx@xx-xxx.xxx.neon.xxxx/xxxxdxx?sslmode=require",
"uploadButton": "Загрузить локальные данные на удаленный сервер",
"uploadHint": "После подключения к удаленной базе данных загрузите и объедините локальную историю SQLite на удаленный сервер.",
"uploadNeedRemote": "Сначала переключитесь и подключитесь к удаленной базе данных PostgreSQL",
"noNeedUpload": "Локальные и удаленные данные уже согласованы",
"uploadConfirmTitle": "Подтвердить загрузку локальных данных на удаленный сервер?",
"uploadConfirmContent": "Синхронизация будет отдавать приоритет локальным данным. Обнаружено: только локальные {{localOnly}}, только удаленные {{remoteOnly}}, конфликты {{conflicts}}.",
"uploadSuccess": "Локальные данные загружены на удаленный сервер",
"uploadFailed": "Ошибка загрузки"
},
"data": {
"title": "Управление данными",
"settlement": "Расчет",
"settlementAndRestart": "Рассчитать и перезапустить",
"settlementHint": "Классифицирует нерассчитанные записи в один этап и сбрасывает все баллы студентов в ноль.",
"importExport": "Импорт / Экспорт",
"exportJson": "Экспорт JSON",
"importJson": "Импорт JSON",
"importHint": "Импорт перезапишет существующих студентов/причины/записи/настройки (настройки безопасности исключены).",
"logs": "Журналы",
"logLevel": "Уровень журнала",
"logOperation": "Операция с журналами",
"viewLogs": "Просмотр журналов",
"exportLogs": "Экспорт журналов",
"clearLogs": "Очистить журналы",
"logsCleared": "Журналы очищены",
"systemLogs": "Системные журналы (последние 200)",
"noLogs": "Нет журналов",
"logLevelUpdated": "Уровень журнала обновлен",
"readLogsFailed": "Ошибка чтения журналов",
"logsExported": "Журналы экспортированы",
"exportFailed": "Ошибка экспорта",
"exportSuccess": "Экспорт успешен",
"importSuccess": "Импорт успешен, обновление",
"importFailed": "Ошибка импорта",
"clearFailed": "Ошибка очистки",
"confirmSettlement": "Подтвердить расчет и перезапуск?",
"settlementConfirm1": "Это архивирует нерассчитанные записи в один этап и сбросит все баллы студентов в ноль.",
"settlementConfirm2": "Список студентов остается без изменений; история расчетов доступна на странице «История расчетов».",
"settlementSuccess": "Расчет успешен, баллы перезапущены",
"settlementFailed": "Ошибка расчета",
"settle": "Рассчитать",
"logLevels": {
"debug": "DEBUG (отладка)",
"info": "INFO (информация)",
"warn": "WARN (предупреждение)",
"error": "ERROR (ошибка)"
}
},
"url": {
"title": "Протокол URL (secscore://)",
"protocol": "Протокол URL",
"description": "Вы можете вызвать SecScore через URL для выполнения операций, например:",
"register": "Зарегистрировать протокол URL",
"registered": "Протокол URL зарегистрирован",
"registerFailed": "Ошибка регистрации",
"installerRequired": "Требуется установленная версия SecScore, может не работать в режиме разработки."
},
"mcp": {
"title": "Сервис MCP",
"description": "Управление встроенным HTTP-сервисом MCP для внешних клиентов MCP.",
"running": "Запущен",
"stopped": "Остановлен",
"noUrl": "Нет URL",
"host": "Хост",
"port": "Порт",
"start": "Запустить",
"stop": "Остановить",
"refresh": "Обновить состояние",
"hint": "Рекомендуется привязывать только к 127.0.0.1; после запуска конечная точка - http://host:port/mcp",
"hostRequired": "Пожалуйста, введите хост MCP",
"portInvalid": "Пожалуйста, введите действительный порт (1-65535)",
"startSuccess": "Сервер MCP запущен",
"startFailed": "Ошибка запуска сервера MCP",
"stopSuccess": "Сервер MCP остановлен",
"stopFailed": "Ошибка остановки сервера MCP",
"startTimeout": "Время ожидания запуска сервера MCP истекло, повторите попытку",
"stopTimeout": "Время ожидания остановки сервера MCP истекло, повторите попытку"
},
"app": {
"title": "Управление приложением",
"description": "Быстро перезапустить или закрыть приложение.",
"restart": "Перезапустить приложение",
"quit": "Закрыть приложение",
"confirmRestartTitle": "Подтвердить перезапуск приложения?",
"confirmRestartContent": "Приложение будет немедленно закрыто и перезапущено.",
"confirmQuitTitle": "Подтвердить закрытие приложения?",
"confirmQuitContent": "Приложение будет немедленно закрыто.",
"restartFailed": "Ошибка перезапуска",
"quitFailed": "Ошибка закрытия"
},
"about": {
"title": "О программе",
"appName": "Управление образовательными баллами",
"version": "Версия",
"copyright": "Авторские права",
"ipcStatus": "Статус IPC",
"ipcConnected": "Подключено",
"ipcDisconnected": "Отключено (Ошибка предварительной загрузки)",
"environment": "Среда",
"toggleDevTools": "Переключить инструменты разработчика",
"license": "SecScore распространяется по лицензии GPL3.0",
"copyRightText": "CopyRight © 2025-{{year}} SECTL"
}
},
"sidebar": {
"home": "Главная",
"students": "Управление студентами",
"score": "Операции с баллами",
"boards": "Доски",
"leaderboard": "Таблица лидеров",
"settlements": "История расчетов",
"reasons": "Управление причинами",
"autoScore": "Автоматическое добавление баллов",
"rewardExchange": "Обмен наградами",
"rewardSettings": "Настройки наград",
"settings": "Системные настройки",
"remoteDb": "Удаленная база данных",
"refreshStatus": "Обновить состояние",
"syncNow": "Синхронизировать сейчас",
"syncSuccess": "Синхронизация успешна",
"syncFailed": "Ошибка синхронизации",
"getDbStatusFailed": "Ошибка получения статуса базы данных",
"notRemoteMode": "Режим удаленной базы данных не активирован, перезапустите приложение",
"dbNotConnected": "База данных не подключена"
},
"auth": {
"unlock": "Разблокировать разрешение",
"unlockHint": "Введите 6-значный пароль: пароль администратора = полный доступ, пароль баллов = только операции с баллами.",
"unlockButton": "Разблокировать",
"unlocked": "Разрешение разблокировано",
"logout": "Переключено в режим только для чтения",
"passwordPlaceholder": "например, 123456",
"passwordError": "Неверный пароль",
"enterPassword": "Введите пароль",
"lock": "Заблокировать"
},
"languages": {
"zh-CN": "简体中文",
"en-US": "English",
"ja-JP": "日本語",
"fr-FR": "Français",
"es-ES": "Español",
"ko-KR": "한국어",
"ru-RU": "Русский",
"de-DE": "Deutsch",
"pt-BR": "Português",
"ar-SA": "العربية"
},
"theme": {
"colorAndBackground": "Цвет и фон",
"gradientLabels": {
"blue": "Свежий синий",
"pink": "Нежный розовый",
"cyan": "Голубой",
"purple": "Фиолетовый"
},
"gradientDirections": {
"vertical": "Вертикально",
"horizontal": "Горизонтально",
"diagonal": "По диагонали"
},
"generate": "Создать",
"saved": "Сохранено",
"saveFailed": "Ошибка сохранения",
"mode": "Режим",
"myTheme": "Моя тема"
},
"permissions": {
"admin": "Разрешение администратора",
"points": "Разрешение баллов",
"view": "Только для чтения"
},
"home": {
"title": "Домашняя страница баллов студентов",
"subtitle": "Всего {{count}} студентов, нажмите на карточку для выполнения операций",
"searchPlaceholder": "Поиск имени/пиньинь...",
"sortBy": {
"alphabet": "Сортировка по имени",
"surname": "Группа по фамилии",
"group": "Сортировка по группе",
"score": "Рейтинг по баллам"
},
"layoutBy": {
"grouped": "Группированный список",
"squareGrid": "Квадратная сетка"
},
"noStudents": "Нет данных о студентах, пожалуйста, добавьте в управлении студентами",
"noMatch": "Нет совпадающих студентов",
"clearSearch": "Очистить поиск",
"points": "баллы",
"currentScore": "Текущие баллы",
"quickOptions": "Быстрые опции",
"adjustPoints": "Настройка баллов",
"customPoints": "Пользовательские баллы",
"customPointsHint": "Введите любое значение в поле ввода",
"reason": "Причина",
"reasonPlaceholder": "Введите причину изменения баллов (необязательно)",
"preview": "Предпросмотр изменения",
"noReason": "(Без причины)",
"ungrouped": "Без группы",
"category": {
"others": "Другие"
},
"scoreAdded": "{{points}} баллов добавлено {{name}}",
"scoreDeducted": "{{points}} баллов снято у {{name}}",
"submitFailed": "Ошибка отправки",
"pleaseSelectPoints": "Пожалуйста, выберите или введите баллы",
"reasonDefault": "{{action}} {{points}} баллов",
"studentCount": "{{count}} студентов",
"operationTitle": "Операция с баллами: {{name}}",
"submitOperation": "Отправить операцию",
"operationTitleBatch": "Операция с баллами: выбрано {{count}}",
"undoLastAction": "Отменить",
"undoLastHint": "Отменить последнее: {{name}} {{delta}}",
"undoUnavailable": "Нет операций с баллами для отмены",
"undoLastSuccess": "Последняя операция с баллами отменена",
"multiSelect": "Множественный выбор",
"batchMode": "Пакетный режим",
"selectAll": "Выбрать все",
"clearSelected": "Очистить выбор",
"batchOperate": "Пакетные баллы",
"selected": "Выбрано",
"selectedCount": "Выбрано {{count}}",
"batchSuccess": "Баллы отправлены для {{count}} студентов",
"batchPartial": "Успешно отправлено {{success}}/{{total}} студентов",
"selectStudentFirst": "Пожалуйста, сначала выберите одного или нескольких студентов",
"addPoints": "Добавить баллы",
"deductPoints": "Снять баллы",
"pointsChange": "Изменение баллов",
"reasonLabel": "Причина: "
},
"students": {
"title": "Управление студентами",
"importList": "Импорт списка",
"addStudent": "Добавить студента",
"name": "Имя",
"group": "Группа",
"avatar": "Аватар",
"currentScore": "Текущие баллы",
"tags": "Теги",
"noTags": "Нет тегов",
"editTags": "Редактировать теги",
"editAvatar": "Настроить аватар",
"deleteConfirm": "Подтвердить удаление этого студента?",
"addTitle": "Добавить студента",
"addConfirm": "Добавить",
"namePlaceholder": "Введите имя студента",
"groupPlaceholder": "Введите название группы (необязательно)",
"nameRequired": "Пожалуйста, введите имя",
"nameExists": "Имя студента уже существует",
"addSuccess": "Добавлено успешно",
"addFailed": "Ошибка добавления",
"deleteSuccess": "Удалено успешно",
"deleteFailed": "Ошибка удаления",
"tagSaveSuccess": "Теги сохранены успешно",
"tagSaveFailed": "Ошибка сохранения тегов",
"importTitle": "Импорт списка",
"importTextHint": "Вставьте несколько строк, одно имя студента на строку",
"importTextPlaceholder": "Пример:\nИван\nМария\nПётр",
"importByText": "Импорт из текста",
"importByXlsx": "Импорт через xlsx",
"importByBanyou": "Импорт из BanYou",
"xlsxPreview": "Предпросмотр и импорт xlsx",
"file": "Файл",
"selectNameCol": "Нажмите на заголовок для выбора столбца с именами",
"previewRows": "Предпросмотр первых 50 строк",
"importConfirm": "Импорт",
"importComplete": "Импорт завершен: добавлено {{inserted}}, пропущено {{skipped}}",
"workerNotReady": "Рабочий не инициализирован",
"parseXlsxFailed": "Ошибка анализа xlsx",
"selectNameColFirst": "Пожалуйста, сначала выберите «Столбец с именами»",
"noNamesFound": "В выбранном столбце не найдено импортируемых имен",
"importFailed": "Ошибка импорта",
"banyouCookieHint": "После входа в BanYou Web скопируйте полную куку и вставьте ниже.",
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
"banyouCookieRequired": "Пожалуйста, сначала вставьте куку",
"banyouFetch": "Получить список классов",
"banyouFetchSuccess": "Получено {{count}} классов",
"banyouFetchFailed": "Ошибка получения классов BanYou",
"banyouCreatedClasses": "Мои созданные классы",
"banyouJoinedClasses": "Мои присоединенные классы",
"banyouNoClasses": "Нет данных о классах"
},
"boards": {
"title": "Управление досками",
"addBoard": "Добавить доску",
"editBoard": "Редактировать доску",
"deleteBoard": "Удалить доску",
"name": "Название доски",
"description": "Описание",
"visibility": "Видимость",
"public": "Публичная",
"private": "Частная",
"studentsCount": "{{count}} студентов",
"noBoards": "Нет досок",
"addTitle": "Добавить доску",
"editTitle": "Редактировать доску",
"namePlaceholder": "Введите название доски",
"descriptionPlaceholder": "Введите описание",
"confirmDelete": "Подтвердить удаление этой доски?",
"saved": "Сохранено",
"saveFailed": "Ошибка сохранения",
"deleted": "Удалено",
"deleteFailed": "Ошибка удаления",
"boardExists": "Доска уже существует"
},
"leaderboard": {
"title": "Таблица лидеров",
"sortBy": "Сортировать по",
"score": "Баллы",
"name": "Имя",
"rank": "Ранг",
"student": "Студент",
"points": "Баллы",
"noData": "Нет данных рейтинга",
"topStudents": "Лучшие студенты",
"rankChanged": "Ранг: {{rank}}"
},
"settlements": {
"title": "История расчетов",
"phase": "Этап",
"date": "Дата",
"studentCount": "Количество студентов",
"totalPoints": "Всего баллов",
"actions": "Действия",
"viewDetails": "Просмотреть детали",
"noSettlements": "Нет расчетов",
"phaseDetail": "Детали этапа {{phase}}",
"settlementDate": "Дата расчета: {{date}}",
"totalStudents": "Всего студентов: {{count}}",
"totalPointsChange": "Общее изменение баллов: {{points}}"
},
"reasons": {
"title": "Управление причинами",
"addReason": "Добавить причину",
"editReason": "Редактировать причину",
"name": "Название причины",
"points": "Баллы",
"positive": "Положительный",
"negative": "Отрицательный",
"noReasons": "Нет причин",
"addTitle": "Добавить причину",
"editTitle": "Редактировать причину",
"namePlaceholder": "Введите название причины",
"pointsPlaceholder": "Введите баллы",
"confirmDelete": "Подтвердить удаление этой причины?",
"saved": "Сохранено",
"saveFailed": "Ошибка сохранения",
"deleted": "Удалено",
"deleteFailed": "Ошибка удаления",
"reasonExists": "Причина уже существует"
},
"autoScore": {
"title": "Автоматическое добавление баллов",
"addRule": "Добавить правило",
"editRule": "Редактировать правило",
"deleteRule": "Удалить правило",
"ruleName": "Название правила",
"trigger": "Триггер",
"action": "Действие",
"interval": "Интервал",
"enabled": "Включено",
"noRules": "Нет правил",
"addTitle": "Добавить правило",
"editTitle": "Редактировать правило",
"namePlaceholder": "Введите название правила",
"triggerPlaceholder": "Выберите триггер",
"actionPlaceholder": "Выберите действие",
"intervalPlaceholder": "Выберите интервал",
"confirmDelete": "Подтвердить удаление этого правила?",
"saved": "Сохранено",
"saveFailed": "Ошибка сохранения",
"deleted": "Удалено",
"deleteFailed": "Ошибка удаления",
"ruleExists": "Правило уже существует"
},
"rewardExchange": {
"title": "Обмен наградами",
"rewards": "Награды",
"exchange": "Обмен",
"history": "История",
"noRewards": "Нет наград",
"noHistory": "Нет истории",
"rewardName": "Название награды",
"cost": "Стоимость",
"stock": "Склад",
"exchangeSuccess": "Обмен успешен",
"exchangeFailed": "Ошибка обмена",
"insufficientPoints": "Недостаточно баллов",
"outOfStock": "Нет в наличии"
},
"rewardSettings": {
"title": "Настройки наград",
"addReward": "Добавить награду",
"editReward": "Редактировать награду",
"deleteReward": "Удалить награду",
"name": "Название",
"cost": "Стоимость",
"stock": "Склад",
"noRewards": "Нет наград",
"addTitle": "Добавить награду",
"editTitle": "Редактировать награду",
"namePlaceholder": "Введите название",
"costPlaceholder": "Введите стоимость",
"stockPlaceholder": "Введите склад",
"confirmDelete": "Подтвердить удаление этой награды?",
"saved": "Сохранено",
"saveFailed": "Ошибка сохранения",
"deleted": "Удалено",
"deleteFailed": "Ошибка удаления",
"rewardExists": "Награда уже существует"
},
"recovery": {
"title": "Строка восстановления SecScore",
"saved": "Я сохранил",
"export": "Экспорт текстового файла",
"exportHint": "Рекомендуется экспортировать и хранить в автономном режиме. При потере восстановить невозможно.",
"hint": "Рекомендуется экспортировать и хранить в автономном режиме после экспорта. При потере восстановить невозможно."
}
}
+300
View File
@@ -0,0 +1,300 @@
/**
* SECTL
* 使 KV SecScore
*/
import { sectlKVStorage } from "./sectlKVStorage"
import { sectlAuth } from "./sectlAuth"
// 数据结构定义
export interface ScoreData {
studentId: string
studentName: string
currentScore: number
lastUpdated: string
}
export interface ScoreEvent {
id: string
studentId: string
scoreChange: number
reason: string
createdAt: string
}
export interface SyncedData {
version: string
lastSyncTime: string
students: ScoreData[]
events: ScoreEvent[]
settings: {
reasons: any[]
tags: any[]
}
}
// 数据键名
const DATA_KEYS = {
STUDENTS: "secscore_students",
EVENTS: "secscore_events",
SETTINGS: "secscore_settings",
SYNC_META: "secscore_sync_metadata",
}
class ScoreSyncService {
/**
*
*/
async syncStudents(students: any[]): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
// 转换数据格式
const scoreData: ScoreData[] = students.map((student) => ({
studentId: student.id,
studentName: student.name,
currentScore: student.score || 0,
lastUpdated: new Date().toISOString(),
}))
// 保存到云端
await sectlKVStorage.setKV(DATA_KEYS.STUDENTS, scoreData, {
is_json: true,
})
console.log("学生数据同步成功")
} catch (error: any) {
console.error("同步学生数据失败:", error)
throw new Error(`同步失败:${error.message}`)
}
}
/**
*
*/
async loadStudents(): Promise<ScoreData[]> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const result = await sectlKVStorage.getKV<ScoreData[]>(DATA_KEYS.STUDENTS)
return result.value || []
} catch (error: any) {
if (error.message.includes("不存在")) {
return []
}
throw error
}
}
/**
*
*/
async syncEvents(events: any[]): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const scoreEvents: ScoreEvent[] = events.map((event) => ({
id: event.id,
studentId: event.student_id,
scoreChange: event.score_change,
reason: event.reason,
createdAt: event.created_at,
}))
await sectlKVStorage.setKV(DATA_KEYS.EVENTS, scoreEvents, {
is_json: true,
})
console.log("积分事件同步成功")
} catch (error: any) {
console.error("同步积分事件失败:", error)
throw new Error(`同步失败:${error.message}`)
}
}
/**
*
*/
async loadEvents(): Promise<ScoreEvent[]> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const result = await sectlKVStorage.getKV<ScoreEvent[]>(DATA_KEYS.EVENTS)
return result.value || []
} catch (error: any) {
if (error.message.includes("不存在")) {
return []
}
throw error
}
}
/**
*
*/
async syncSettings(settings: { reasons: any[]; tags: any[] }): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
await sectlKVStorage.setKV(DATA_KEYS.SETTINGS, settings, {
is_json: true,
})
console.log("设置数据同步成功")
} catch (error: any) {
console.error("同步设置数据失败:", error)
throw new Error(`同步失败:${error.message}`)
}
}
/**
*
*/
async loadSettings(): Promise<{ reasons: any[]; tags: any[] }> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const result = await sectlKVStorage.getKV(DATA_KEYS.SETTINGS)
return result.value || { reasons: [], tags: [] }
} catch (error: any) {
if (error.message.includes("不存在")) {
return { reasons: [], tags: [] }
}
throw error
}
}
/**
*
*/
async updateSyncMetadata(): Promise<void> {
const metadata = {
version: "1.0",
lastSyncTime: new Date().toISOString(),
platform: "secscore",
}
await sectlKVStorage.setKV(DATA_KEYS.SYNC_META, metadata, {
is_json: true,
})
}
/**
*
*/
async getSyncMetadata(): Promise<{
version: string
lastSyncTime: string
platform: string
} | null> {
try {
const result = await sectlKVStorage.getKV(DATA_KEYS.SYNC_META)
return result.value
} catch {
return null
}
}
/**
*
*/
async fullSync(data: {
students: any[]
events: any[]
settings: { reasons: any[]; tags: any[] }
}): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
await this.syncStudents(data.students)
await this.syncEvents(data.events)
await this.syncSettings(data.settings)
await this.updateSyncMetadata()
console.log("完整数据同步成功")
} catch (error: any) {
console.error("完整数据同步失败:", error)
throw error
}
}
/**
*
*/
async restoreFromCloud(): Promise<{
students: ScoreData[]
events: ScoreEvent[]
settings: { reasons: any[]; tags: any[] }
metadata: any
}> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
const [students, events, settings, metadata] = await Promise.all([
this.loadStudents(),
this.loadEvents(),
this.loadSettings(),
this.getSyncMetadata(),
])
return { students, events, settings, metadata }
} catch (error: any) {
console.error("从云端恢复数据失败:", error)
throw error
}
}
/**
*
*/
async hasCloudData(): Promise<boolean> {
try {
const students = await this.loadStudents()
return students.length > 0
} catch {
return false
}
}
/**
*
*/
async clearCloudData(): Promise<void> {
if (!sectlAuth.isAuthenticated()) {
throw new Error("未授权,请先登录")
}
try {
await Promise.all([
sectlKVStorage.deleteKV(DATA_KEYS.STUDENTS),
sectlKVStorage.deleteKV(DATA_KEYS.EVENTS),
sectlKVStorage.deleteKV(DATA_KEYS.SETTINGS),
sectlKVStorage.deleteKV(DATA_KEYS.SYNC_META),
])
console.log("云端数据已清除")
} catch (error: any) {
console.error("清除云端数据失败:", error)
throw error
}
}
}
// 导出单例
export const scoreSyncService = new ScoreSyncService()
+24
View File
@@ -0,0 +1,24 @@
/**
* SECTL
*/
export { sectlAuth, SECTL_CONFIG } from "./sectlAuth"
export type { TokenData, UserInfo } from "./sectlAuth"
export { sectlCloudStorage } from "./sectlCloudStorage"
export type { CloudFile, ShareLink, StorageUsage } from "./sectlCloudStorage"
export { sectlKVStorage } from "./sectlKVStorage"
export type { KVData } from "./sectlCloudStorage"
export { sectlNotification } from "./sectlNotification"
export type { Notification, SendNotificationParams } from "./sectlNotification"
// 积分数据同步服务
export { scoreSyncService } from "./scoreSyncService"
export type { ScoreData, ScoreEvent, SyncedData } from "./scoreSyncService"
+388
View File
@@ -0,0 +1,388 @@
/**
* SECTL OAuth 2.0
* SECTL-One-Stop OAuth API
* PKCE (Proof Key for Code Exchange)
*/
import { Client, Account } from "appwrite"
import CryptoJS from "crypto-js"
// SECTL API 配置
export const SECTL_CONFIG = {
baseUrl: "https://appwrite.sectl.top",
authUrl: "https://sectl.top",
platformId: "", // 需要在设置中配置
callbackUrl: "http://localhost:5173/auth/callback",
callbackPort: 5173,
}
// Token 数据类型
export interface TokenData {
access_token: string
refresh_token: string
token_type: string
expires_in: number
user_id: string
}
// 用户信息类型
export interface UserInfo {
user_id: string
email: string
name: string
github_username?: string
permission: string
role: string
avatar_url?: string
background_url?: string
bio: string
tags: string[]
platform_id?: string
login_time?: string
}
// PKCE 相关工具函数
function generateCodeVerifier(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return base64URLEncode(array)
}
function generateCodeChallenge(verifier: string): string {
const sha256 = CryptoJS.SHA256(verifier)
return base64URLEncode(CryptoJS.enc.Base64.parse(sha256.toString(CryptoJS.enc.Base64)))
}
function base64URLEncode(buffer: Uint8Array | CryptoJS.lib.WordArray): string {
let base64: string
if (buffer instanceof Uint8Array) {
base64 = btoa(String.fromCharCode(...buffer))
} else {
base64 = buffer.toString(CryptoJS.enc.Base64)
}
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
}
class SectlAuthService {
private client: Client
private tokenData: TokenData | null = null
private codeVerifier: string = ""
constructor() {
this.client = new Client()
new Account(this.client)
this.loadToken()
}
/**
*
*/
initialize(platformId: string, callbackUrl?: string) {
SECTL_CONFIG.platformId = platformId
if (callbackUrl) {
SECTL_CONFIG.callbackUrl = callbackUrl
}
this.client.setEndpoint(SECTL_CONFIG.baseUrl).setProject("sectl-cloud") // 项目 ID
return this
}
/**
* URL
*/
getAuthorizationUrl(scope: string[] = ["user:read"]): string {
this.codeVerifier = generateCodeVerifier()
const codeChallenge = generateCodeChallenge(this.codeVerifier)
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
redirect_uri: SECTL_CONFIG.callbackUrl,
response_type: "code",
code_challenge: codeChallenge,
code_challenge_method: "S256",
scope: scope.join(" "),
})
return `${SECTL_CONFIG.authUrl}/oauth/authorize?${params.toString()}`
}
/**
* OAuth
*/
async authorize(scope: string[] = ["user:read"]): Promise<TokenData> {
// 生成 PKCE 参数
this.codeVerifier = generateCodeVerifier()
// codeChallenge 在 getAuthorizationUrl 中生成
// 构建授权 URL
const authUrl = this.getAuthorizationUrl(scope)
// 在新窗口中打开授权页面
const authWindow = window.open(authUrl, "_blank", "width=600,height=700")
// 等待回调
return new Promise((resolve, reject) => {
// 监听授权回调
const checkCallback = async () => {
try {
// 检查是否有授权码(通过 postMessage 或 URL 参数)
window.addEventListener("message", async (event) => {
if (event.data.type === "oauth-callback" && event.data.code) {
try {
const token = await this.exchangeCodeForToken(event.data.code, scope)
authWindow?.close()
resolve(token)
} catch (error) {
reject(error)
}
}
})
// 轮询检查窗口关闭
const checkInterval = setInterval(() => {
if (authWindow?.closed) {
clearInterval(checkInterval)
reject(new Error("授权窗口已关闭"))
}
}, 1000)
} catch (error) {
reject(error)
}
}
checkCallback()
})
}
/**
* 使 Token
*/
async exchangeCodeForToken(code: string, scope: string[] = ["user:read"]): Promise<TokenData> {
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/token`
const payload = {
grant_type: "authorization_code",
code,
client_id: SECTL_CONFIG.platformId,
redirect_uri: SECTL_CONFIG.callbackUrl,
code_verifier: this.codeVerifier,
scope: scope.join(" "),
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "Token 交换失败")
}
const data: TokenData = await response.json()
this.tokenData = data
this.saveToken()
return data
} catch (error) {
console.error("Token 交换失败:", error)
throw error
}
}
/**
*
*/
async getUserInfo(): Promise<UserInfo> {
if (!this.tokenData?.access_token) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/userinfo`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${this.tokenData.access_token}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取用户信息失败")
}
return await response.json()
} catch (error) {
console.error("获取用户信息失败:", error)
throw error
}
}
/**
* Token
*/
async introspectToken(token?: string): Promise<{ active: boolean; user_id?: string }> {
const tokenToCheck = token || this.tokenData?.access_token
if (!tokenToCheck) {
return { active: false }
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/introspect`
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token: tokenToCheck,
client_id: SECTL_CONFIG.platformId,
}),
})
if (!response.ok) {
return { active: false }
}
return await response.json()
} catch (error) {
console.error("Token 验证失败:", error)
return { active: false }
}
}
/**
* Token
*/
async refreshAccessToken(): Promise<TokenData> {
if (!this.tokenData?.refresh_token) {
throw new Error("没有刷新令牌")
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/token`
const payload = {
grant_type: "refresh_token",
refresh_token: this.tokenData.refresh_token,
client_id: SECTL_CONFIG.platformId,
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "Token 刷新失败")
}
const data: TokenData = await response.json()
this.tokenData = data
this.saveToken()
return data
} catch (error) {
console.error("Token 刷新失败:", error)
throw error
}
}
/**
* Token
*/
async logout(): Promise<void> {
if (!this.tokenData?.access_token) {
return
}
const url = `${SECTL_CONFIG.baseUrl}/api/oauth/logout`
try {
await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${this.tokenData.access_token}`,
},
})
} catch (error) {
console.error("登出失败:", error)
} finally {
this.tokenData = null
this.clearToken()
}
}
/**
* Token
*/
getToken(): TokenData | null {
return this.tokenData
}
/**
* Access Token
*/
getAccessToken(): string | null {
return this.tokenData?.access_token || null
}
/**
*
*/
isAuthenticated(): boolean {
return !!this.tokenData?.access_token
}
/**
* Token
*/
private saveToken(): void {
if (this.tokenData) {
localStorage.setItem("sectl_token", JSON.stringify(this.tokenData))
localStorage.setItem("sectl_code_verifier", this.codeVerifier)
}
}
/**
* Token
*/
private loadToken(): void {
try {
const tokenStr = localStorage.getItem("sectl_token")
const codeVerifier = localStorage.getItem("sectl_code_verifier")
if (tokenStr) {
this.tokenData = JSON.parse(tokenStr)
this.codeVerifier = codeVerifier || ""
}
} catch (error) {
console.error("加载 Token 失败:", error)
}
}
/**
* Token
*/
private clearToken(): void {
localStorage.removeItem("sectl_token")
localStorage.removeItem("sectl_code_verifier")
this.tokenData = null
this.codeVerifier = ""
}
}
// 导出单例
export const sectlAuth = new SectlAuthService()
+596
View File
@@ -0,0 +1,596 @@
/**
* SECTL
*
*/
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
// 文件信息类型
export interface CloudFile {
file_id: string
filename: string
extension: string
size: number
size_formatted: string
created_at: string
updated_at: string
url: string
thumbnail_url?: string
}
// 分享信息类型
export interface ShareLink {
share_id: string
share_url: string
file_id: string
filename: string
expires_at?: string
has_password: boolean
view_count?: number
download_count?: number
status: "active" | "disabled" | "expired"
}
// KV 存储类型
export interface KVData {
kv_id: string
key: string
value: any
is_json: boolean
size: number
created_at: string
updated_at: string
}
// 存储使用情况
export interface StorageUsage {
used_storage: number
used_storage_formatted: string
total_storage: number
total_storage_formatted: string
available_storage: number
available_storage_formatted: string
percentage: number
file_count: number
}
class SectlCloudStorageService {
/**
*
*/
async listFiles(options?: {
folder_id?: string
limit?: number
offset?: number
}): Promise<{ files: CloudFile[]; total: number; has_more: boolean }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
limit: String(options?.limit || 100),
offset: String(options?.offset || 0),
})
if (options?.folder_id) {
params.append("folder_id", options.folder_id)
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取文件列表失败")
}
return await response.json()
} catch (error) {
console.error("获取文件列表失败:", error)
throw error
}
}
/**
*
*/
async uploadFile(
file: File | Blob,
options?: {
folder_id?: string
filename?: string
}
): Promise<{
success: boolean
file_id: string
filename: string
size: number
size_formatted: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const formData = new FormData()
formData.append("client_id", SECTL_CONFIG.platformId)
formData.append("user_id", sectlAuth.getToken()?.user_id || "")
if (options?.folder_id) {
formData.append("folder_id", options.folder_id)
}
// 如果是 File 对象,直接使用;否则需要指定文件名
if (file instanceof File) {
formData.append("file", file)
} else {
const filename = options?.filename || "upload_" + Date.now()
formData.append("file", file, filename)
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/upload`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
body: formData,
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "文件上传失败")
}
return await response.json()
} catch (error) {
console.error("文件上传失败:", error)
throw error
}
}
/**
* Base64
*/
async uploadFileBase64(
base64Data: string,
filename: string,
mimeType: string = "application/octet-stream",
options?: {
folder_id?: string
}
): Promise<{
success: boolean
file_id: string
filename: string
size: number
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/upload`
const payload = {
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
filename,
mime_type: mimeType,
file: base64Data,
folder_id: options?.folder_id,
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "文件上传失败")
}
return await response.json()
} catch (error) {
console.error("文件上传失败:", error)
throw error
}
}
/**
*
*/
async downloadFile(fileId: string): Promise<{ download_url: string; expires_at: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}/download?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取下载链接失败")
}
return await response.json()
} catch (error) {
console.error("获取下载链接失败:", error)
throw error
}
}
/**
*
*/
async previewFile(fileId: string): Promise<{ view_url: string; expires_at: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}/preview?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取预览链接失败")
}
return await response.json()
} catch (error) {
console.error("获取预览链接失败:", error)
throw error
}
}
/**
*
*/
async deleteFile(fileId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除文件失败")
}
return await response.json()
} catch (error) {
console.error("删除文件失败:", error)
throw error
}
}
/**
*
*/
async renameFile(
fileId: string,
newFilename: string
): Promise<{
file_id: string
filename: string
updated_at: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/files/${fileId}`
try {
const response = await fetch(url, {
method: "PUT",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
filename: newFilename,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "重命名文件失败")
}
return await response.json()
} catch (error) {
console.error("重命名文件失败:", error)
throw error
}
}
/**
*
*/
async createShare(
fileId: string,
options?: {
expires_in?: number // 秒,0 表示永久
password?: string
}
): Promise<{
share_id: string
share_url: string
file_id: string
filename: string
expires_at?: string
has_password: boolean
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/share`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
file_id: fileId,
expires_in: options?.expires_in || 86400, // 默认 1 天
password: options?.password,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "创建分享失败")
}
return await response.json()
} catch (error) {
console.error("创建分享失败:", error)
throw error
}
}
/**
*
*/
async listShares(): Promise<{ shares: ShareLink[]; total: number }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取分享列表失败")
}
return await response.json()
} catch (error) {
console.error("获取分享列表失败:", error)
throw error
}
}
/**
*
*/
async disableShare(shareId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares/${shareId}/disable`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "禁用分享失败")
}
return await response.json()
} catch (error) {
console.error("禁用分享失败:", error)
throw error
}
}
/**
*
*/
async enableShare(shareId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares/${shareId}/enable`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "启用分享失败")
}
return await response.json()
} catch (error) {
console.error("启用分享失败:", error)
throw error
}
}
/**
*
*/
async deleteShare(shareId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/shares/${shareId}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除分享失败")
}
return await response.json()
} catch (error) {
console.error("删除分享失败:", error)
throw error
}
}
/**
* 使
*/
async getStorageUsage(): Promise<StorageUsage> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/storage/usage?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取存储使用情况失败")
}
return await response.json()
} catch (error) {
console.error("获取存储使用情况失败:", error)
throw error
}
}
}
// 导出单例
export const sectlCloudStorage = new SectlCloudStorageService()
+256
View File
@@ -0,0 +1,256 @@
/**
* SECTL KV
* JSON
*/
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
import { KVData } from "./sectlCloudStorage"
class SectlKVStorageService {
/**
*
*/
async setKV(
key: string,
value: any,
options?: {
is_json?: boolean
}
): Promise<{
success: boolean
kv_id: string
key: string
value: any
is_json: boolean
size: number
created_at: string
updated_at: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv`
// 自动检测是否为 JSON 格式
let valueStr: string
let isJson = options?.is_json
if (typeof value === "object") {
valueStr = JSON.stringify(value)
if (isJson === undefined) {
isJson = true
}
} else {
valueStr = String(value)
if (isJson === undefined) {
// 尝试检测是否为 JSON 字符串
try {
JSON.parse(valueStr)
isJson = true
} catch {
isJson = false
}
}
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
key,
value: valueStr,
is_json: isJson,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "设置键值对失败")
}
return await response.json()
} catch (error) {
console.error("设置键值对失败:", error)
throw error
}
}
/**
*
*/
async getKV<T = any>(
key: string,
options?: {
field?: string // 获取 JSON 中的特定字段
}
): Promise<KVData | { key: string; field: string; value: T }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
})
if (options?.field) {
params.append("field", options.field)
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取键值对失败")
}
return await response.json()
} catch (error) {
console.error("获取键值对失败:", error)
throw error
}
}
/**
*
*/
async listKV(options?: {
limit?: number
offset?: number
}): Promise<{ kvs: KVData[]; total: number; has_more: boolean }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
limit: String(options?.limit || 100),
offset: String(options?.offset || 0),
})
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取键值对列表失败")
}
return await response.json()
} catch (error) {
console.error("获取键值对列表失败:", error)
throw error
}
}
/**
* JSON
*/
async updateKVField(
key: string,
field: string,
value: any
): Promise<{
success: boolean
key: string
field: string
value: any
updated_at: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}`
try {
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
field,
value,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "更新字段失败")
}
return await response.json()
} catch (error) {
console.error("更新字段失败:", error)
throw error
}
}
/**
*
*/
async deleteKV(key: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: SECTL_CONFIG.platformId,
user_id: sectlAuth.getToken()?.user_id || "",
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除键值对失败")
}
return await response.json()
} catch (error) {
console.error("删除键值对失败:", error)
throw error
}
}
}
// 导出单例
export const sectlKVStorage = new SectlKVStorageService()
+225
View File
@@ -0,0 +1,225 @@
/**
* SECTL
*
*/
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
// 通知类型
export interface Notification {
$id: string
user_id: string
title: string
content?: string
type: string
source?: string
source_id?: string
is_read: boolean
created_at: string
read_at?: string
action_url?: string
icon?: string
priority: "low" | "normal" | "high"
expires_at?: string
}
// 发送通知参数
export interface SendNotificationParams {
user_id: string
title: string
content?: string
type?: string
source_id?: string
action_url?: string
icon?: string
priority?: "low" | "normal" | "high"
expires_at?: string
}
class SectlNotificationService {
/**
*
*/
async getNotifications(options?: {
limit?: number
offset?: number
unread_only?: boolean
}): Promise<{ notifications: Notification[]; total: number }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const params = new URLSearchParams({
limit: String(options?.limit || 20),
offset: String(options?.offset || 0),
})
if (options?.unread_only) {
params.append("unread_only", "true")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications?${params.toString()}`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "获取通知列表失败")
}
return await response.json()
} catch (error) {
console.error("获取通知列表失败:", error)
throw error
}
}
/**
*
*/
async markNotificationRead(notificationId: string): Promise<{
success: boolean
notification: {
$id: string
is_read: boolean
read_at: string
}
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/${notificationId}/read`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "标记通知已读失败")
}
return await response.json()
} catch (error) {
console.error("标记通知已读失败:", error)
throw error
}
}
/**
*
*/
async markAllNotificationsRead(): Promise<{ success: boolean; marked_count: number }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/read-all`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "标记所有通知已读失败")
}
return await response.json()
} catch (error) {
console.error("标记所有通知已读失败:", error)
throw error
}
}
/**
*
*/
async deleteNotification(notificationId: string): Promise<{ success: boolean; message: string }> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/${notificationId}`
try {
const response = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "删除通知失败")
}
return await response.json()
} catch (error) {
console.error("删除通知失败:", error)
throw error
}
}
/**
*
*/
async sendNotification(params: SendNotificationParams): Promise<{
success: boolean
notification_id: string
message: string
}> {
const accessToken = sectlAuth.getAccessToken()
if (!accessToken) {
throw new Error("未授权,请先登录")
}
const url = `${SECTL_CONFIG.baseUrl}/api/notifications/send`
try {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
...params,
client_id: SECTL_CONFIG.platformId,
}),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error_description || "发送通知失败")
}
return await response.json()
} catch (error) {
console.error("发送通知失败:", error)
throw error
}
}
}
// 导出单例
export const sectlNotification = new SectlNotificationService()
+18
View File
@@ -0,0 +1,18 @@
/**
* SECTL
* SecScore
*/
import React from "react"
import { SectlProvider } from "../contexts/SectlContext"
import { SectlSettingsPanel } from "../components/SectlSettingsPanel"
export const ScoreSyncView: React.FC = () => {
return (
<SectlProvider>
<div style={{ padding: 24 }}>
<SectlSettingsPanel />
</div>
</SectlProvider>
)
}