mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
feat: 添加SECTL云服务集成功能
实现SECTL云服务的全面集成,包括: 1. 添加OAuth认证服务,支持PKCE流程 2. 实现云存储管理功能 3. 添加积分数据同步服务 4. 实现通知服务和KV存储服务 5. 添加多语言支持 6. 创建相关React组件和上下文 7. 更新依赖项添加crypto-js 8. 优化HTTP客户端重用
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
Generated
+16
@@ -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:
|
||||
|
||||
@@ -354,10 +354,12 @@ 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),
|
||||
@@ -396,8 +398,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,
|
||||
@@ -431,8 +436,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!({
|
||||
@@ -463,8 +471,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))
|
||||
@@ -493,8 +504,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!({
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -33,6 +35,11 @@ impl AppState {
|
||||
let logger = Arc::new(RwLock::new(LoggerService::new()));
|
||||
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,
|
||||
@@ -44,6 +51,7 @@ impl AppState {
|
||||
auto_score,
|
||||
logger,
|
||||
data,
|
||||
http_client,
|
||||
app_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 控制台创建应用后填写平台 ID(Client 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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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 = (() => {
|
||||
|
||||
@@ -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": "يوصى بالحفظ خارج الخط بعد التصدير. في حالة الضياع، لا يمكن استعادته."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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é."
|
||||
}
|
||||
}
|
||||
@@ -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": "エクスポート後にオフライン保存を推奨。紛失すると復旧できません。"
|
||||
}
|
||||
}
|
||||
@@ -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": "내보낸 후 오프라인으로 저장하는 것이 좋습니다. 분실 시 복구할 수 없습니다."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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": "Рекомендуется экспортировать и хранить в автономном режиме после экспорта. При потере восстановить невозможно."
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user