Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/common/DateRangePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ const displayValue = computed(() => {

const formatDate = (dateStr: string): string => {
const date = new Date(dateStr + 'T00:00:00')
const dateLocale = locale.value === 'zh' ? 'zh-CN' : 'en-US'
const dateLocale = locale.value === 'zh' ? 'zh-CN' : locale.value === 'es' ? 'es-ES' : 'en-US'
return date.toLocaleDateString(dateLocale, { month: 'short', day: 'numeric' })
}

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/__tests__/localesMessageCompile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { baseCompile } from '@intlify/message-compiler'

import en from '../locales/en'
import zh from '../locales/zh'
import es from '../locales/es'

// vue-i18n 在运行时才编译消息:文案里未转义的花括号(如内嵌 JSON 示例
// "{\"user-agent\": ...}")会在渲染时抛 "Invalid token in placeholder",
Expand Down Expand Up @@ -32,7 +33,8 @@ function collectCompileErrors(node: unknown, path: string, out: string[]): void
describe('locale messages compile', () => {
it.each([
['zh', zh],
['en', en]
['en', en],
['es', es]
] as const)('%s messages all compile without placeholder errors', (locale, messages) => {
const errors: string[] = []
collectCompileErrors(messages, locale, errors)
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createI18n } from 'vue-i18n'

type LocaleCode = 'en' | 'zh'
type LocaleCode = 'en' | 'zh' | 'es'

type LocaleMessages = Record<string, any>

Expand All @@ -9,11 +9,12 @@ const DEFAULT_LOCALE: LocaleCode = 'en'

const localeLoaders: Record<LocaleCode, () => Promise<{ default: LocaleMessages }>> = {
en: () => import('./locales/en'),
zh: () => import('./locales/zh')
zh: () => import('./locales/zh'),
es: () => import('./locales/es')
}

function isLocaleCode(value: string): value is LocaleCode {
return value === 'en' || value === 'zh'
return value === 'en' || value === 'zh' || value === 'es'
}

function getDefaultLocale(): LocaleCode {
Expand All @@ -26,6 +27,9 @@ function getDefaultLocale(): LocaleCode {
if (browserLang.startsWith('zh')) {
return 'zh'
}
if (browserLang.startsWith('es')) {
return 'es'
}

return DEFAULT_LOCALE
}
Expand Down Expand Up @@ -93,7 +97,8 @@ export function getLocale(): LocaleCode {

export const availableLocales = [
{ code: 'en', name: 'English', flag: '🇺🇸' },
{ code: 'zh', name: '中文', flag: '🇨🇳' }
{ code: 'zh', name: '中文', flag: '🇨🇳' },
{ code: 'es', name: 'Español', flag: '🇪🇸' }
] as const

export default i18n
1,506 changes: 1,506 additions & 0 deletions frontend/src/i18n/locales/es/admin/accounts.ts

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions frontend/src/i18n/locales/es/admin/audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export default {
audit: {
title: 'Registros de auditoría',
description: 'Registra las operaciones de administración realizadas por administradores y usuarios. De las credenciales enviadas en las cabeceras solo se conservan los primeros y últimos caracteres, y el contenido de las peticiones se censura. Las entradas no se pueden borrar una por una; vaciarlas todas requiere verificación en dos pasos.',
clearAll: 'Vaciar todo',
empty: 'Todavía no hay registros de auditoría',
loadFailed: 'No se pudieron cargar los registros de auditoría',
filters: {
all: 'Todos',
q: 'Palabra clave',
qPlaceholder: 'Ruta / acción / correo del actor',
actorEmail: 'Correo del actor',
action: 'Acción',
clientIp: 'IP del cliente',
method: 'Método',
authMethod: 'Método de autenticación',
result: 'Resultado',
resultSuccess: 'Correcto',
resultFailure: 'Fallido',
startTime: 'Hora de inicio',
endTime: 'Hora de fin'
},
columns: {
time: 'Hora',
actor: 'Actor',
action: 'Acción',
method: 'Método',
result: 'Resultado',
clientIp: 'IP del cliente',
detail: 'Detalle'
},
detail: {
title: 'Detalle del registro de auditoría',
actorRole: 'Rol',
methodPath: 'Método / Ruta',
latency: 'Latencia',
requestId: 'ID de la petición',
credential: 'Credencial (enmascarada)',
userAgent: 'User-Agent',
requestBody: 'Cuerpo de la petición (censurado)',
extra: 'Adicional'
},
clearConfirm: {
title: 'Vaciar todos los registros de auditoría',
message: 'Esto elimina de forma permanente todos los registros de auditoría y no se puede deshacer. La propia acción de vaciado queda registrada. ¿Continuar?',
totpTitle: 'Introduce el código de dos pasos',
totpHint: 'Vaciar los registros de auditoría requiere una verificación TOTP reciente.',
success: 'Se vaciaron {count} registro(s) de auditoría',
failed: 'No se pudieron vaciar los registros de auditoría'
}
}
}
Loading
Loading