feat(i18n): 为评论系统添加国际化支持

- 在管理后台引入 vue-i18n 并添加多语言文件
- 为前端评论组件添加翻译功能,支持自动语言检测
- 在功能设置中新增管理后台和组件语言配置选项
- 更新 API 接口以支持语言设置存储
- 优化菜单项文本溢出显示样式
This commit is contained in:
anghunk
2026-02-10 14:26:38 +08:00
parent 860830cee2
commit 4d9e2d8d31
39 changed files with 6023 additions and 332 deletions

View File

@@ -1,5 +1,5 @@
{
"name": "docs",
"name": "cwd-docs",
"description": "CWD 评论系统文档",
"type": "module",
"license": "Apache-2.0",
@@ -7,7 +7,7 @@
"dev": "vitepress dev",
"build:docs": "vitepress build",
"build:widget": "cd widget && npm install && npm run build",
"build": "npm run build:widget && node copy-cwd-to-public.js && npm run build:docs",
"build": "npm run build:widget && node build.js && npm run build:docs",
"preview": "vitepress preview"
},
"devDependencies": {

View File

@@ -3,6 +3,7 @@ import { Component } from './Component.js';
export class AdminAuthModal extends Component {
constructor(container, props = {}) {
super(container, props);
this.t = props.t || ((k) => k);
this.state = {
key: '',
error: '',
@@ -24,16 +25,16 @@ export class AdminAuthModal extends Component {
this.createElement('div', {
className: 'cwd-modal',
children: [
this.createTextElement('h3', '管理员身份验证', 'cwd-modal-title'),
this.createTextElement('h3', this.t('verifyAdminTitle'), 'cwd-modal-title'),
this.createElement('div', {
className: 'cwd-modal-body',
children: [
this.createTextElement('p', '检测到管理员邮箱,请输入密钥以继续。', 'cwd-modal-desc'),
this.createTextElement('p', this.t('verifyAdminDesc'), 'cwd-modal-desc'),
this.createElement('input', {
className: `cwd-form-input ${error ? 'cwd-input-error' : ''}`,
attributes: {
type: 'password',
placeholder: '请输入管理员密钥',
placeholder: this.t('adminKeyPlaceholder'),
value: key,
disabled: loading,
onInput: (e) => this.setState({ key: e.target.value, error: '' }),
@@ -50,7 +51,7 @@ export class AdminAuthModal extends Component {
children: [
this.createElement('button', {
className: 'cwd-btn cwd-btn-secondary',
text: '取消',
text: this.t('cancel'),
attributes: {
type: 'button',
disabled: loading,
@@ -59,7 +60,7 @@ export class AdminAuthModal extends Component {
}),
this.createElement('button', {
className: 'cwd-btn cwd-btn-primary',
text: loading ? '验证中...' : '验证',
text: loading ? this.t('verifying') : this.t('verify'),
attributes: {
type: 'button',
disabled: loading || !key,
@@ -116,7 +117,7 @@ export class AdminAuthModal extends Component {
if (this.elements.submitBtn) {
this.elements.submitBtn.disabled = loading || !key;
this.elements.submitBtn.textContent = loading ? '验证中...' : '验证';
this.elements.submitBtn.textContent = loading ? this.t('verifying') : this.t('verify');
}
}
@@ -126,7 +127,7 @@ export class AdminAuthModal extends Component {
this.setState({ loading: true });
this.props.onSubmit(this.state.key)
.catch(err => {
this.setState({ error: err.message || '验证失败', loading: false });
this.setState({ error: err.message || this.t('verifyFailed'), loading: false });
});
}
}

View File

@@ -21,6 +21,7 @@ export class CommentForm extends Component {
*/
constructor(container, props = {}) {
super(container, props);
this.t = props.t || ((k) => k);
// 确保 localForm 的各个字段都有初始值
const initialForm = props.form || {};
this.state = {
@@ -53,7 +54,7 @@ export class CommentForm extends Component {
},
children: [
// 标题
this.createTextElement('h3', '发表评论', 'cwd-form-title'),
this.createTextElement('h3', this.t('formTitle'), 'cwd-form-title'),
// 表单字段
this.createElement('div', {
@@ -64,19 +65,19 @@ export class CommentForm extends Component {
className: 'cwd-form-row',
children: [
// 昵称
this.createFormField('昵称 *', 'text', 'name', localForm.name, formErrors.name),
this.createFormField(this.t('nickname'), 'text', 'name', localForm.name, formErrors.name),
// 邮箱
this.createElement('div', {
className: 'cwd-form-field-wrapper',
children: [
this.createFormField('邮箱 *', 'email', 'email', localForm.email, formErrors.email),
this.createFormField(this.t('email'), 'email', 'email', localForm.email, formErrors.email),
isVerified
? this.createElement('div', {
className: 'cwd-admin-controls',
children: [
this.createElement('button', {
className: 'cwd-btn-text',
text: '退出验证',
text: this.t('verifyAdmin'),
attributes: {
type: 'button',
title: '清除管理员凭证',
@@ -92,7 +93,7 @@ export class CommentForm extends Component {
],
}),
// 网址
this.createFormField('网址', 'url', 'url', localForm.url, formErrors.url),
this.createFormField(this.t('website'), 'url', 'url', localForm.url, formErrors.url),
],
}),
@@ -100,7 +101,7 @@ export class CommentForm extends Component {
this.createElement('div', {
className: 'cwd-form-field',
children: [
this.createTextElement('label', '写下你的评论...', 'cwd-form-label'),
this.createTextElement('label', this.t('writeComment'), 'cwd-form-label'),
this.createElement('textarea', {
className: `cwd-form-textarea ${formErrors.content ? 'cwd-input-error' : ''}`,
attributes: {
@@ -129,7 +130,7 @@ export class CommentForm extends Component {
style: localForm.content?.trim() ? '' : 'display:none;',
onClick: () => this.togglePreview(),
},
text: this.state.showPreview ? '关闭' : '预览',
text: this.state.showPreview ? this.t('close') : this.t('preview'),
}),
this.createElement('button', {
className: 'cwd-btn cwd-btn-primary',
@@ -137,7 +138,7 @@ export class CommentForm extends Component {
type: 'submit',
disabled: submitting || !canSubmit,
},
text: submitting ? '提交中...' : '提交评论',
text: submitting ? this.t('submitting') : this.t('submit'),
}),
],
}),
@@ -209,7 +210,7 @@ export class CommentForm extends Component {
const submitBtn = this.elements.root.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = submitting || !canSubmit;
submitBtn.textContent = submitting ? '提交中...' : '提交评论';
submitBtn.textContent = submitting ? this.t('submitting') : this.t('submit');
}
// 更新预览按钮状态
@@ -224,9 +225,9 @@ export class CommentForm extends Component {
if (previewContainer) {
previewContainer.remove();
}
previewBtn.textContent = '预览';
previewBtn.textContent = this.t('preview');
} else {
previewBtn.textContent = this.state.showPreview ? '关闭' : '预览';
previewBtn.textContent = this.state.showPreview ? this.t('close') : this.t('preview');
}
}
@@ -416,6 +417,7 @@ export class CommentForm extends Component {
this.modal = null;
}
},
t: this.t
});
this.modal.render();
}

View File

@@ -31,6 +31,7 @@ export class CommentItem extends Component {
*/
constructor(container, props = {}) {
super(container, props);
this.t = props.t || ((k) => k);
this.replyEditor = null;
this.childCommentItems = []; // 缓存嵌套回复的 CommentItem 实例
}
@@ -103,7 +104,7 @@ export class CommentItem extends Component {
// 显示回复目标
...(comment.replyToAuthor
? [
this.createTextElement('span', ' 回复 ', 'cwd-reply-to-separator'),
this.createTextElement('span', ` ${this.t('reply')} `, 'cwd-reply-to-separator'),
this.createTextElement('span', comment.replyToAuthor, 'cwd-reply-to-author'),
]
: []),
@@ -119,7 +120,7 @@ export class CommentItem extends Component {
attributes: {
onClick: () => this.handleReply(),
},
text: '回复',
text: this.t('reply'),
}),
...(this.props.enableCommentLike !== false
? [
@@ -169,7 +170,7 @@ export class CommentItem extends Component {
}),
]
: []),
this.createTextElement('span', formatRelativeTime(comment.created), 'cwd-comment-time'),
this.createTextElement('span', formatRelativeTime(comment.created, this.t), 'cwd-comment-time'),
],
}),
],
@@ -220,6 +221,7 @@ export class CommentItem extends Component {
onCancel: () => this.handleCancelReply(),
onClearError: () => this.handleClearReplyError(),
placeholder: this.props.replyPlaceholder,
t: this.t
});
this.replyEditor.render();
this.replyEditor.focus();
@@ -253,6 +255,7 @@ export class CommentItem extends Component {
onCancelReply: this.props.onCancelReply,
onUpdateReplyContent: this.props.onUpdateReplyContent,
onClearReplyError: this.props.onClearReplyError,
t: this.t
});
replyItem.render();
this.childCommentItems.push(replyItem);

View File

@@ -106,7 +106,8 @@ export class CommentList extends Component {
onCancelReply: () => this.handleCancelReply(),
onUpdateReplyContent: (content) => this.handleUpdateReplyContent(content),
onClearReplyError: () => this.handleClearReplyError(),
onLikeComment: (commentId, isLike) => this.handleLikeComment(commentId, isLike)
onLikeComment: (commentId, isLike) => this.handleLikeComment(commentId, isLike),
t: this.props.t
});
commentItem.render();
// 缓存 CommentItem 实例

View File

@@ -20,6 +20,7 @@ export class ReplyEditor extends Component {
*/
constructor(container, props = {}) {
super(container, props);
this.t = props.t || ((k) => k);
const { currentUser } = props;
this.state = {
content: props.content || '',
@@ -41,7 +42,7 @@ export class ReplyEditor extends Component {
this.createElement('div', {
className: 'cwd-reply-header',
children: [
this.createTextElement('span', `回复 @${this.props.replyToAuthor}`, 'cwd-reply-to'),
this.createTextElement('span', `${this.t('reply')} @${this.props.replyToAuthor}`, 'cwd-reply-to'),
this.createElement('button', {
className: 'cwd-btn-close',
attributes: {
@@ -62,9 +63,9 @@ export class ReplyEditor extends Component {
style: 'margin-bottom: 12px;',
},
children: [
this.createFormField('昵称 *', 'text', 'name', currentUser?.name),
this.createFormField('邮箱 *', 'email', 'email', currentUser?.email),
this.createFormField('网址', 'url', 'url', currentUser?.url),
this.createFormField(this.t('nickname'), 'text', 'name', currentUser?.name),
this.createFormField(this.t('email'), 'email', 'email', currentUser?.email),
this.createFormField(this.t('website'), 'url', 'url', currentUser?.url),
],
}),
]
@@ -113,7 +114,7 @@ export class ReplyEditor extends Component {
disabled: this.props.submitting || !this.state.content.trim(),
onClick: () => this.togglePreview(),
},
text: this.state.showPreview ? '关闭' : '预览',
text: this.state.showPreview ? this.t('close') : this.t('preview'),
}),
this.createElement('button', {
className: 'cwd-btn cwd-btn-primary cwd-btn-small',
@@ -122,7 +123,7 @@ export class ReplyEditor extends Component {
disabled: this.props.submitting || !this.state.content.trim(),
onClick: () => this.handleSubmit(),
},
text: this.props.submitting ? '提交中...' : '提交回复',
text: this.props.submitting ? this.t('submitting') : this.t('submit'),
}),
this.createElement('button', {
className: 'cwd-btn cwd-btn-secondary cwd-btn-small',
@@ -131,7 +132,7 @@ export class ReplyEditor extends Component {
disabled: this.props.submitting,
onClick: () => this.handleCancel(),
},
text: '取消',
text: this.t('cancel'),
}),
],
}),

View File

@@ -9,6 +9,7 @@ import { CommentForm } from '@/components/CommentForm.js';
import { CommentList } from '@/components/CommentList.js';
import { ImagePreview } from '@/components/ImagePreview.js';
import styles from '@/styles/main.css?inline';
import { locales } from '@/locales/index.js';
/**
* CWDComments 评论组件主类
@@ -60,6 +61,20 @@ export class CWDComments {
this._likeCountEl = null;
this._mounted = false;
this.localeData = locales['zh-CN'];
this.t = this.t.bind(this);
}
/**
* 翻译函数
*/
t(key, params = {}) {
let text = this.localeData[key] || key;
for (const [k, v] of Object.entries(params)) {
text = text.replace(`{${k}}`, v);
}
return text;
}
/**
@@ -109,6 +124,7 @@ export class CWDComments {
enableImageLightbox: typeof data.enableImageLightbox === 'boolean' ? data.enableImageLightbox : false,
commentPlaceholder:
typeof data.commentPlaceholder === 'string' ? data.commentPlaceholder : undefined,
widgetLanguage: typeof data.widgetLanguage === 'string' ? data.widgetLanguage : undefined,
};
} catch (e) {
return {};
@@ -152,16 +168,44 @@ export class CWDComments {
return currentHostname === domain || currentHostname.endsWith('.' + domain);
});
// 设置语言
let lang = serverConfig.widgetLanguage || 'auto';
if (lang === 'auto' && typeof navigator !== 'undefined') {
const browserLang = navigator.language || navigator.userLanguage;
if (browserLang.toLowerCase().startsWith('en')) {
lang = 'en-US';
} else {
lang = 'zh-CN';
}
}
if (locales[lang]) {
this.localeData = locales[lang];
}
if (!isAllowed) {
if (this.mountPoint) {
this.mountPoint.innerHTML = `
<div style="padding: 20px; text-align: center; color: #666; font-size: 14px; border: 1px solid #eee; border-radius: 8px; background: #f9f9f9;">
当前域名 (${currentHostname}) 未获得评论组件调用授权
${this.t('domainUnauthorized', { domain: currentHostname })}
</div>
`;
}
return;
}
} else {
// 即使没有域名限制,也需要设置语言
let lang = serverConfig.widgetLanguage || 'auto';
if (lang === 'auto' && typeof navigator !== 'undefined') {
const browserLang = navigator.language || navigator.userLanguage;
if (browserLang.toLowerCase().startsWith('en')) {
lang = 'en-US';
} else {
lang = 'zh-CN';
}
}
if (locales[lang]) {
this.localeData = locales[lang];
}
}
if (serverConfig.avatarPrefix) {
@@ -341,7 +385,7 @@ export class CWDComments {
<path d="M12 21c-.4 0-.8-.1-1.1-.4L4.5 15C3 13.6 2 11.7 2 9.6 2 6.5 4.5 4 7.6 4c1.7 0 3.3.8 4.4 2.1C13.1 4.8 14.7 4 16.4 4 19.5 4 22 6.5 22 9.6c0 2.1-1 4-2.5 5.4l-6.4 5.6c-.3.3-.7.4-1.1.4z"></path>
</svg>
</span>
<div>已有 <span class="cwd-like-count">0</span>人喜欢~ </div>
<div>${this.t('likes', { count: '<span class="cwd-like-count">0</span>' })}</div>
</button>
</div>
`;
@@ -366,6 +410,7 @@ export class CWDComments {
adminEmail: this.config.adminEmail,
onVerifyAdmin: (key) => this.api.verifyAdminKey(key),
placeholder: this.config.commentPlaceholder,
t: this.t
});
this.commentForm.render();
}
@@ -376,7 +421,7 @@ export class CWDComments {
countHeading = document.createElement('h3');
countHeading.className = 'cwd-comments-count';
countHeading.innerHTML = `
<span class="cwd-comments-count-number">0</span> 条评论
${this.t('totalComments', { count: '<span class="cwd-comments-count-number">0</span>' })}
`;
if (this.formContainer && this.formContainer.parentNode === this.mountPoint) {
this.mountPoint.insertBefore(countHeading, this.formContainer.nextSibling);
@@ -424,6 +469,7 @@ export class CWDComments {
this.store.likeComment(commentId, isLike);
}
},
t: this.t
});
this.commentList.render();
}

View File

@@ -0,0 +1,632 @@
export const locales = {
'en-US': {
submit: 'Submit',
submitting: 'Submitting...',
preview: 'Preview',
close: 'Close',
nickname: 'Name *',
email: 'Email *',
website: 'Website',
writeComment: 'Write a comment...',
reply: 'Reply',
cancel: 'Cancel',
admin: 'Admin',
like: 'Like',
liked: 'Liked',
likes: '{count} Likes',
comments: 'Comments',
totalComments: '{count} Comments',
domainUnauthorized: 'Domain ({domain}) not authorized',
errorTitle: 'Error',
successTitle: 'Success',
verifyAdmin: 'Sign out',
formTitle: 'Leave a Comment',
loadMore: 'Load More',
noComments: 'No comments yet',
delete: 'Delete',
confirmDelete: 'Are you sure you want to delete this comment?',
deleteSuccess: 'Deleted successfully',
deleteFailed: 'Failed to delete',
adminKeyPlaceholder: 'Enter admin key',
verifyAdminTitle: 'Admin Verification',
verifyAdminDesc: 'Please enter admin key to continue',
verify: 'Verify',
verifying: 'Verifying...',
verifyFailed: 'Verification failed, please try again',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Yesterday',
'time.daysAgo': '{count} days ago',
'time.hoursAgo': '{count} hours ago',
'time.minutesAgo': '{count} minutes ago',
'time.justNow': 'Just now',
},
'zh-CN': {
submit: '提交评论',
submitting: '提交中...',
preview: '预览',
close: '关闭',
nickname: '昵称 *',
email: '邮箱 *',
website: '网址',
writeComment: '写下你的评论...',
reply: '回复',
cancel: '取消',
admin: '博主',
like: '喜欢',
liked: '已赞',
likes: '{count}人喜欢',
comments: '条评论',
totalComments: '共 {count} 条评论',
domainUnauthorized: '当前域名 ({domain}) 未获得评论组件调用授权',
errorTitle: '错误',
successTitle: '成功',
verifyAdmin: '退出验证',
formTitle: '发表评论',
loadMore: '加载更多',
noComments: '暂无评论',
delete: '删除',
confirmDelete: '确定删除这条评论吗?',
deleteSuccess: '删除成功',
deleteFailed: '删除失败',
adminKeyPlaceholder: '输入密钥以设置或修改',
verifyAdminTitle: '管理员验证',
verifyAdminDesc: '请输入管理员密钥以继续',
verify: '验证',
verifying: '验证中...',
verifyFailed: '验证失败,请重试',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': '昨天',
'time.daysAgo': '{count}天前',
'time.hoursAgo': '{count}小时前',
'time.minutesAgo': '{count}分钟前',
'time.justNow': '刚刚',
},
'zh-TW': {
submit: '提交評論',
submitting: '提交中...',
preview: '預覽',
close: '關閉',
nickname: '暱稱 *',
email: '信箱 *',
website: '網址',
writeComment: '寫下你的評論...',
reply: '回覆',
cancel: '取消',
admin: '站長',
like: '喜歡',
liked: '已讚',
likes: '{count}人喜歡',
comments: '條評論',
totalComments: '共 {count} 條評論',
domainUnauthorized: '當前域名 ({domain}) 未獲得評論組件調用授權',
errorTitle: '錯誤',
successTitle: '成功',
verifyAdmin: '退出驗證',
formTitle: '發表評論',
loadMore: '加載更多',
noComments: '暫無評論',
delete: '刪除',
confirmDelete: '確定刪除這條評論嗎?',
deleteSuccess: '刪除成功',
deleteFailed: '刪除失敗',
adminKeyPlaceholder: '輸入密鑰以設置或修改',
verifyAdminTitle: '管理員驗證',
verifyAdminDesc: '請輸入管理員密鑰以繼續',
verify: '驗證',
verifying: '驗證中...',
verifyFailed: '驗證失敗,請重試',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': '昨天',
'time.daysAgo': '{count}天前',
'time.hoursAgo': '{count}小時前',
'time.minutesAgo': '{count}分鐘前',
'time.justNow': '剛剛',
},
es: {
submit: 'Enviar',
submitting: 'Enviando...',
preview: 'Vista previa',
close: 'Cerrar',
nickname: 'Nombre *',
email: 'Correo *',
website: 'Sitio web',
writeComment: 'Escribe un comentario...',
reply: 'Responder',
cancel: 'Cancelar',
admin: 'Admin',
like: 'Me gusta',
liked: 'Te gusta',
likes: '{count} Me gusta',
comments: 'Comentarios',
totalComments: '{count} Comentarios',
domainUnauthorized: 'Dominio ({domain}) no autorizado',
errorTitle: 'Error',
successTitle: 'Éxito',
verifyAdmin: 'Cerrar sesión',
formTitle: 'Deja un comentario',
loadMore: 'Cargar más',
noComments: 'Sin comentarios aún',
delete: 'Eliminar',
confirmDelete: '¿Estás seguro de querer eliminar este comentario?',
deleteSuccess: 'Eliminado con éxito',
deleteFailed: 'Fallo al eliminar',
adminKeyPlaceholder: 'Ingresa clave de admin',
verifyAdminTitle: 'Verificación de Admin',
verifyAdminDesc: 'Por favor ingresa la clave de admin para continuar',
verify: 'Verificar',
verifying: 'Verificando...',
verifyFailed: 'Verificación fallida, intenta de nuevo',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Ayer',
'time.daysAgo': 'Hace {count} días',
'time.hoursAgo': 'Hace {count} horas',
'time.minutesAgo': 'Hace {count} minutos',
'time.justNow': 'Justo ahora',
},
pt: {
submit: 'Enviar',
submitting: 'Enviando...',
preview: 'Prévia',
close: 'Fechar',
nickname: 'Nome *',
email: 'Email *',
website: 'Site',
writeComment: 'Escreva um comentário...',
reply: 'Responder',
cancel: 'Cancelar',
admin: 'Admin',
like: 'Curtir',
liked: 'Curtido',
likes: '{count} Curtidas',
comments: 'Comentários',
totalComments: '{count} Comentários',
domainUnauthorized: 'Domínio ({domain}) não autorizado',
errorTitle: 'Erro',
successTitle: 'Sucesso',
verifyAdmin: 'Sair',
formTitle: 'Deixe um comentário',
loadMore: 'Carregar mais',
noComments: 'Sem comentários ainda',
delete: 'Excluir',
confirmDelete: 'Tem certeza que deseja excluir este comentário?',
deleteSuccess: 'Excluído com sucesso',
deleteFailed: 'Falha ao excluir',
adminKeyPlaceholder: 'Digite a chave de admin',
verifyAdminTitle: 'Verificação de Admin',
verifyAdminDesc: 'Por favor digite a chave de admin para continuar',
verify: 'Verificar',
verifying: 'Verificando...',
verifyFailed: 'Verificação falhou, tente novamente',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Ontem',
'time.daysAgo': 'Há {count} dias',
'time.hoursAgo': 'Há {count} horas',
'time.minutesAgo': 'Há {count} minutos',
'time.justNow': 'Agora mesmo',
},
fr: {
submit: 'Envoyer',
submitting: 'Envoi...',
preview: 'Aperçu',
close: 'Fermer',
nickname: 'Nom *',
email: 'Email *',
website: 'Site web',
writeComment: 'Écrire un commentaire...',
reply: 'Répondre',
cancel: 'Annuler',
admin: 'Admin',
like: "J'aime",
liked: 'Aimé',
likes: "{count} J'aime",
comments: 'Commentaires',
totalComments: '{count} Commentaires',
domainUnauthorized: 'Domaine ({domain}) non autorisé',
errorTitle: 'Erreur',
successTitle: 'Succès',
verifyAdmin: 'Déconnexion',
formTitle: 'Laisser un commentaire',
loadMore: 'Charger plus',
noComments: "Aucun commentaire pour l'instant",
delete: 'Supprimer',
confirmDelete: 'Êtes-vous sûr de vouloir supprimer ce commentaire ?',
deleteSuccess: 'Supprimé avec succès',
deleteFailed: 'Échec de la suppression',
adminKeyPlaceholder: 'Entrer la clé admin',
verifyAdminTitle: 'Vérification Admin',
verifyAdminDesc: 'Veuillez entrer la clé admin pour continuer',
verify: 'Vérifier',
verifying: 'Vérification...',
verifyFailed: 'Échec de la vérification, réessayez',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Hier',
'time.daysAgo': 'Il y a {count} jours',
'time.hoursAgo': 'Il y a {count} heures',
'time.minutesAgo': 'Il y a {count} minutes',
'time.justNow': "À l'instant",
},
de: {
submit: 'Senden',
submitting: 'Senden...',
preview: 'Vorschau',
close: 'Schließen',
nickname: 'Name *',
email: 'E-Mail *',
website: 'Webseite',
writeComment: 'Schreibe einen Kommentar...',
reply: 'Antworten',
cancel: 'Abbrechen',
admin: 'Admin',
like: 'Gefällt mir',
liked: 'Gefällt mir',
likes: '{count} Gefällt mir',
comments: 'Kommentare',
totalComments: '{count} Kommentare',
domainUnauthorized: 'Domain ({domain}) nicht autorisiert',
errorTitle: 'Fehler',
successTitle: 'Erfolg',
verifyAdmin: 'Abmelden',
formTitle: 'Hinterlasse einen Kommentar',
loadMore: 'Mehr laden',
noComments: 'Noch keine Kommentare',
delete: 'Löschen',
confirmDelete: 'Möchtest du diesen Kommentar wirklich löschen?',
deleteSuccess: 'Erfolgreich gelöscht',
deleteFailed: 'Löschen fehlgeschlagen',
adminKeyPlaceholder: 'Admin-Schlüssel eingeben',
verifyAdminTitle: 'Admin-Verifizierung',
verifyAdminDesc: 'Bitte gib den Admin-Schlüssel ein',
verify: 'Verifizieren',
verifying: 'Verifiziere...',
verifyFailed: 'Verifizierung fehlgeschlagen, bitte erneut versuchen',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Gestern',
'time.daysAgo': 'Vor {count} Tagen',
'time.hoursAgo': 'Vor {count} Stunden',
'time.minutesAgo': 'Vor {count} Minuten',
'time.justNow': 'Gerade eben',
},
ja: {
submit: '送信',
submitting: '送信中...',
preview: 'プレビュー',
close: '閉じる',
nickname: '名前 *',
email: 'メール *',
website: 'ウェブサイト',
writeComment: 'コメントを書く...',
reply: '返信',
cancel: 'キャンセル',
admin: '管理者',
like: 'いいね',
liked: 'いいね済み',
likes: '{count} いいね',
comments: 'コメント',
totalComments: '{count} コメント',
domainUnauthorized: 'ドメイン ({domain}) は許可されていません',
errorTitle: 'エラー',
successTitle: '成功',
verifyAdmin: 'ログアウト',
formTitle: 'コメントを残す',
loadMore: 'もっと読み込む',
noComments: 'まだコメントはありません',
delete: '削除',
confirmDelete: 'このコメントを削除してもよろしいですか?',
deleteSuccess: '削除しました',
deleteFailed: '削除に失敗しました',
adminKeyPlaceholder: '管理者キーを入力',
verifyAdminTitle: '管理者認証',
verifyAdminDesc: '続行するには管理者キーを入力してください',
verify: '認証',
verifying: '認証中...',
verifyFailed: '認証に失敗しました。再試行してください',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': '昨日',
'time.daysAgo': '{count}日前',
'time.hoursAgo': '{count}時間前',
'time.minutesAgo': '{count}分前',
'time.justNow': 'たった今',
},
ko: {
submit: '제출',
submitting: '제출 중...',
preview: '미리보기',
close: '닫기',
nickname: '이름 *',
email: '이메일 *',
website: '웹사이트',
writeComment: '댓글을 작성하세요...',
reply: '답글',
cancel: '취소',
admin: '관리자',
like: '좋아요',
liked: '좋아요 함',
likes: '{count} 좋아요',
comments: '댓글',
totalComments: '{count} 댓글',
domainUnauthorized: '도메인 ({domain}) 승인되지 않음',
errorTitle: '오류',
successTitle: '성공',
verifyAdmin: '로그아웃',
formTitle: '댓글 남기기',
loadMore: '더 보기',
noComments: '아직 댓글이 없습니다',
delete: '삭제',
confirmDelete: '이 댓글을 삭제하시겠습니까?',
deleteSuccess: '삭제 성공',
deleteFailed: '삭제 실패',
adminKeyPlaceholder: '관리자 키 입력',
verifyAdminTitle: '관리자 인증',
verifyAdminDesc: '계속하려면 관리자 키를 입력하세요',
verify: '인증',
verifying: '인증 중...',
verifyFailed: '인증 실패, 다시 시도하세요',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': '어제',
'time.daysAgo': '{count}일 전',
'time.hoursAgo': '{count}시간 전',
'time.minutesAgo': '{count}분 전',
'time.justNow': '방금',
},
ru: {
submit: 'Отправить',
submitting: 'Отправка...',
preview: 'Предпросмотр',
close: 'Закрыть',
nickname: 'Имя *',
email: 'Email *',
website: 'Сайт',
writeComment: 'Напишите комментарий...',
reply: 'Ответить',
cancel: 'Отмена',
admin: 'Админ',
like: 'Нравится',
liked: 'Понравилось',
likes: '{count} лайков',
comments: 'Комментарии',
totalComments: '{count} Комментариев',
domainUnauthorized: 'Домен ({domain}) не авторизован',
errorTitle: 'Ошибка',
successTitle: 'Успешно',
verifyAdmin: 'Выйти',
formTitle: 'Оставить комментарий',
loadMore: 'Загрузить еще',
noComments: 'Комментариев пока нет',
delete: 'Удалить',
confirmDelete: 'Вы уверены, что хотите удалить этот комментарий?',
deleteSuccess: 'Успешно удалено',
deleteFailed: 'Ошибка удаления',
adminKeyPlaceholder: 'Введите ключ админа',
verifyAdminTitle: 'Проверка админа',
verifyAdminDesc: 'Пожалуйста, введите ключ админа для продолжения',
verify: 'Проверить',
verifying: 'Проверка...',
verifyFailed: 'Ошибка проверки, попробуйте снова',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Вчера',
'time.daysAgo': '{count} дн. назад',
'time.hoursAgo': '{count} ч. назад',
'time.minutesAgo': '{count} мин. назад',
'time.justNow': 'Только что',
},
it: {
submit: 'Invia',
submitting: 'Invio...',
preview: 'Anteprima',
close: 'Chiudi',
nickname: 'Nome *',
email: 'Email *',
website: 'Sito web',
writeComment: 'Scrivi un commento...',
reply: 'Rispondi',
cancel: 'Annulla',
admin: 'Admin',
like: 'Mi piace',
liked: 'Piaciuto',
likes: '{count} Mi piace',
comments: 'Commenti',
totalComments: '{count} Commenti',
domainUnauthorized: 'Dominio ({domain}) non autorizzato',
errorTitle: 'Errore',
successTitle: 'Successo',
verifyAdmin: 'Esci',
formTitle: 'Lascia un commento',
loadMore: 'Carica altro',
noComments: 'Nessun commento ancora',
delete: 'Elimina',
confirmDelete: 'Sei sicuro di voler eliminare questo commento?',
deleteSuccess: 'Eliminato con successo',
deleteFailed: 'Eliminazione fallita',
adminKeyPlaceholder: 'Inserisci chiave admin',
verifyAdminTitle: 'Verifica Admin',
verifyAdminDesc: 'Inserisci la chiave admin per continuare',
verify: 'Verifica',
verifying: 'Verifica in corso...',
verifyFailed: 'Verifica fallita, riprova',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Ieri',
'time.daysAgo': '{count} giorni fa',
'time.hoursAgo': '{count} ore fa',
'time.minutesAgo': '{count} minuti fa',
'time.justNow': 'Proprio ora',
},
nl: {
submit: 'Verzenden',
submitting: 'Verzenden...',
preview: 'Voorbeeld',
close: 'Sluiten',
nickname: 'Naam *',
email: 'E-mail *',
website: 'Website',
writeComment: 'Schrijf een reactie...',
reply: 'Beantwoorden',
cancel: 'Annuleren',
admin: 'Admin',
like: 'Leuk',
liked: 'Geliket',
likes: '{count} Likes',
comments: 'Reacties',
totalComments: '{count} Reacties',
domainUnauthorized: 'Domein ({domain}) niet geautoriseerd',
errorTitle: 'Fout',
successTitle: 'Succes',
verifyAdmin: 'Uitloggen',
formTitle: 'Laat een reactie achter',
loadMore: 'Meer laden',
noComments: 'Nog geen reacties',
delete: 'Verwijderen',
confirmDelete: 'Weet je zeker dat je deze reactie wilt verwijderen?',
deleteSuccess: 'Succesvol verwijderd',
deleteFailed: 'Verwijderen mislukt',
adminKeyPlaceholder: 'Voer admin-sleutel in',
verifyAdminTitle: 'Admin Verificatie',
verifyAdminDesc: 'Voer de admin-sleutel in om door te gaan',
verify: 'Verifiëren',
verifying: 'Verifiëren...',
verifyFailed: 'Verificatie mislukt, probeer opnieuw',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Gisteren',
'time.daysAgo': '{count} dagen geleden',
'time.hoursAgo': '{count} uur geleden',
'time.minutesAgo': '{count} minuten geleden',
'time.justNow': 'Zojuist',
},
ar: {
submit: 'إرسال',
submitting: 'جاري الإرسال...',
preview: 'معاينة',
close: 'إغلاق',
nickname: 'الاسم *',
email: 'البريد الإلكتروني *',
website: 'الموقع الإلكتروني',
writeComment: 'أكتب تعليقاً...',
reply: 'رد',
cancel: 'إلغاء',
admin: 'مسؤول',
like: 'إعجاب',
liked: 'أعجبني',
likes: '{count} إعجاب',
comments: 'تعليقات',
totalComments: '{count} تعليق',
domainUnauthorized: 'المجال ({domain}) غير مصرح به',
errorTitle: 'خطأ',
successTitle: 'نجاح',
verifyAdmin: 'تسجيل الخروج',
formTitle: 'اترك تعليقاً',
loadMore: 'تحميل المزيد',
noComments: 'لا توجد تعليقات بعد',
delete: 'حذف',
confirmDelete: 'هل أنت متأكد أنك تريد حذف هذا التعليق؟',
deleteSuccess: 'تم الحذف بنجاح',
deleteFailed: 'فشل الحذف',
adminKeyPlaceholder: 'أدخل مفتاح المسؤول',
verifyAdminTitle: 'تحقق المسؤول',
verifyAdminDesc: 'الرجاء إدخال مفتاح المسؤول للمتابعة',
verify: 'تحقق',
verifying: 'جاري التحقق...',
verifyFailed: 'فشل التحقق، حاول مرة أخرى',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'أمس',
'time.daysAgo': 'منذ {count} يوم',
'time.hoursAgo': 'منذ {count} ساعة',
'time.minutesAgo': 'منذ {count} دقيقة',
'time.justNow': 'الآن',
},
hi: {
submit: 'जमा करें',
submitting: 'जमा हो रहा है...',
preview: 'पूर्वावलोकन',
close: 'बंद करें',
nickname: 'नाम *',
email: 'ईमेल *',
website: 'वेबसाइट',
writeComment: 'टिप्पणी लिखें...',
reply: 'उत्तर दें',
cancel: 'रद्द करें',
admin: 'एडमिन',
like: 'पसंद',
liked: 'पसंद किया',
likes: '{count} पसंद',
comments: 'टिप्पणियाँ',
totalComments: '{count} टिप्पणियाँ',
domainUnauthorized: 'डोमेन ({domain}) अधिकृत नहीं है',
errorTitle: 'त्रुटि',
successTitle: 'सफलता',
verifyAdmin: 'साइन आउट',
formTitle: 'टिप्पणी छोड़ें',
loadMore: 'और लोड करें',
noComments: 'अभी कोई टिप्पणी नहीं',
delete: 'हटाएं',
confirmDelete: 'क्या आप वाकई इस टिप्पणी को हटाना चाहते हैं?',
deleteSuccess: 'सफलतापूर्वक हटा दिया गया',
deleteFailed: 'हटाने में विफल',
adminKeyPlaceholder: 'एडमिन कुंजी दर्ज करें',
verifyAdminTitle: 'एडमिन सत्यापन',
verifyAdminDesc: 'जारी रखने के लिए कृपया एडमिन कुंजी दर्ज करें',
verify: 'सत्यापित करें',
verifying: 'सत्यापित हो रहा है...',
verifyFailed: 'सत्यापन विफल, पुनः प्रयास करें',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'कल',
'time.daysAgo': '{count} दिन पहले',
'time.hoursAgo': '{count} घंटे पहले',
'time.minutesAgo': '{count} मिनट पहले',
'time.justNow': 'अभी',
},
id: {
submit: 'Kirim',
submitting: 'Mengirim...',
preview: 'Pratinjau',
close: 'Tutup',
nickname: 'Nama *',
email: 'Email *',
website: 'Situs Web',
writeComment: 'Tulis komentar...',
reply: 'Balas',
cancel: 'Batal',
admin: 'Admin',
like: 'Suka',
liked: 'Disukai',
likes: '{count} Suka',
comments: 'Komentar',
totalComments: '{count} Komentar',
domainUnauthorized: 'Domain ({domain}) tidak diizinkan',
errorTitle: 'Kesalahan',
successTitle: 'Sukses',
verifyAdmin: 'Keluar',
formTitle: 'Tinggalkan Komentar',
loadMore: 'Muat Lebih Banyak',
noComments: 'Belum ada komentar',
delete: 'Hapus',
confirmDelete: 'Apakah Anda yakin ingin menghapus komentar ini?',
deleteSuccess: 'Berhasil dihapus',
deleteFailed: 'Gagal menghapus',
adminKeyPlaceholder: 'Masukkan kunci admin',
verifyAdminTitle: 'Verifikasi Admin',
verifyAdminDesc: 'Silakan masukkan kunci admin untuk melanjutkan',
verify: 'Verifikasi',
verifying: 'Memverifikasi...',
verifyFailed: 'Verifikasi gagal, coba lagi',
clearError: '✕',
clearSuccess: '✕',
'time.yesterday': 'Kemarin',
'time.daysAgo': '{count} hari yang lalu',
'time.hoursAgo': '{count} jam yang lalu',
'time.minutesAgo': '{count} menit yang lalu',
'time.justNow': 'Baru saja',
},
};

View File

@@ -5,9 +5,10 @@
/**
* 格式化时间3天内显示相对时间超过3天显示完整日期
* @param {string|number} dateValue - 日期字符串或时间戳
* @param {Function} t - 翻译函数
* @returns {string}
*/
export function formatRelativeTime(dateValue) {
export function formatRelativeTime(dateValue, t = (k, p) => k) {
const date = new Date(dateValue);
const now = new Date();
const diff = now.getTime() - date.getTime();
@@ -20,15 +21,15 @@ export function formatRelativeTime(dateValue) {
// 3天内显示相对时间
if (days < 3) {
if (days > 0) {
return days === 1 ? '昨天' : `${days}天前`;
return days === 1 ? t('time.yesterday') : t('time.daysAgo', { count: days });
}
if (hours > 0) {
return `${hours}小时前`;
return t('time.hoursAgo', { count: hours });
}
if (minutes > 0) {
return `${minutes}分钟前`;
return t('time.minutesAgo', { count: minutes });
}
return '刚刚';
return t('time.justNow');
}
// 超过3天显示完整日期