mirror of
https://github.com/619dev/Paperphone-plus.git
synced 2026-09-03 07:18:05 +08:00
完善i18n
This commit is contained in:
@@ -2,26 +2,28 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { CameraOff, ChevronLeft, LayoutGrid, Mic, MicOff, PhoneIncoming, PhoneOff, Presentation, Users, Video } from 'lucide-react'
|
||||
import { formatDuration, useGroupCallContext } from '../contexts/GroupCallContext'
|
||||
import { useStore } from '../store'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
|
||||
export default function GroupCallOverlay() {
|
||||
const gc = useGroupCallContext()
|
||||
const { t } = useI18n()
|
||||
const groups = useStore(s => s.groups)
|
||||
const [showPeople, setShowPeople] = useState(false)
|
||||
if (gc.status === 'idle') return gc.error ? <MeetingError message={gc.error} onClose={gc.cleanup} /> : null
|
||||
|
||||
const group = groups.find(g => g.id === gc.groupId)
|
||||
const participants = Array.from(gc.peers.values())
|
||||
const title = gc.groupName || group?.name || '群视频会议'
|
||||
const title = gc.groupName || group?.name || t(gc.isVideo ? 'meeting.video_title' : 'meeting.voice_title')
|
||||
|
||||
if (gc.status === 'ringing') return (
|
||||
<main style={shellStyle}>
|
||||
<div style={{ margin: 'auto', textAlign: 'center' }}>
|
||||
<Avatar name={gc.inviterName} src={gc.inviterAvatar} size={96} />
|
||||
<h2 style={{ margin: '20px 0 8px' }}>{title}</h2>
|
||||
<p style={{ color: '#a9b2c3' }}>{gc.inviterName} 邀请你加入{gc.isVideo ? '视频' : '语音'}会议</p>
|
||||
<p style={{ color: '#a9b2c3' }}>{gc.inviterName} {t('meeting.invites_you')} {t(gc.isVideo ? 'meeting.video' : 'meeting.voice')}</p>
|
||||
<div style={{ display: 'flex', gap: 36, justifyContent: 'center', marginTop: 34 }}>
|
||||
<RoundButton label="拒绝" color="#e5484d" onClick={gc.rejectGroupCall}><PhoneOff /></RoundButton>
|
||||
<RoundButton label="加入" color="#25a65a" onClick={gc.acceptGroupCall}><PhoneIncoming /></RoundButton>
|
||||
<RoundButton label={t('meeting.reject')} color="#e5484d" onClick={gc.rejectGroupCall}><PhoneOff /></RoundButton>
|
||||
<RoundButton label={t('meeting.join')} color="#25a65a" onClick={gc.acceptGroupCall}><PhoneIncoming /></RoundButton>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -30,12 +32,12 @@ export default function GroupCallOverlay() {
|
||||
return (
|
||||
<main style={shellStyle}>
|
||||
<header style={headerStyle}>
|
||||
<button aria-label="离开会议" onClick={gc.leaveGroupCall} style={iconButton}><ChevronLeft /></button>
|
||||
<button aria-label={t('meeting.leave')} onClick={gc.leaveGroupCall} style={iconButton}><ChevronLeft /></button>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 700, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{title}</div>
|
||||
<div style={{ color: '#a9b2c3', fontSize: 12 }}>
|
||||
{gc.status === 'connecting' ? '正在进入会议…' : `${formatDuration(gc.duration)} · ${participants.length + 1}/${gc.maxParticipants} 人`}
|
||||
{gc.meetingMode === 'lecture' && ' · 讲课模式'}
|
||||
{gc.status === 'connecting' ? t('meeting.joining') : `${formatDuration(gc.duration)} · ${participants.length + 1}/${gc.maxParticipants} ${t('meeting.people_unit')}`}
|
||||
{gc.meetingMode === 'lecture' && ` · ${t('meeting.lecture_mode')}`}
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={() => setShowPeople(v => !v)} style={{ ...iconButton, marginLeft: 'auto' }}><Users size={20} /></button>
|
||||
@@ -49,24 +51,25 @@ export default function GroupCallOverlay() {
|
||||
</section>
|
||||
|
||||
<footer style={footerStyle}>
|
||||
<Control label={gc.isMuted ? '解除静音' : '静音'} active={gc.isMuted} disabled={gc.meetingMode === 'lecture' && !gc.isHost} onClick={gc.toggleMute}>
|
||||
<Control label={t(gc.isMuted ? 'meeting.unmute' : 'meeting.mute')} active={gc.isMuted} disabled={gc.meetingMode === 'lecture' && !gc.isHost} onClick={gc.toggleMute}>
|
||||
{gc.isMuted ? <MicOff /> : <Mic />}
|
||||
</Control>
|
||||
{gc.isVideo && <Control label={gc.isCameraOff ? '开启视频' : '关闭视频'} active={gc.isCameraOff} onClick={gc.toggleCamera}>
|
||||
{gc.isVideo && <Control label={t(gc.isCameraOff ? 'meeting.start_video' : 'meeting.stop_video')} active={gc.isCameraOff} onClick={gc.toggleCamera}>
|
||||
{gc.isCameraOff ? <CameraOff /> : <Video />}
|
||||
</Control>}
|
||||
{gc.isHost && <Control label="全员静音" onClick={gc.muteAll}><MicOff /></Control>}
|
||||
{gc.isHost && <Control label={gc.meetingMode === 'lecture' ? '自由讨论' : '讲课模式'} active={gc.meetingMode === 'lecture'}
|
||||
{gc.isHost && <Control label={t('meeting.mute_all')} onClick={gc.muteAll}><MicOff /></Control>}
|
||||
{gc.isHost && <Control label={t(gc.meetingMode === 'lecture' ? 'meeting.discussion_mode' : 'meeting.lecture_mode')} active={gc.meetingMode === 'lecture'}
|
||||
onClick={() => gc.setMeetingMode(gc.meetingMode === 'lecture' ? 'discussion' : 'lecture')}><Presentation /></Control>}
|
||||
<Control label="离开" danger onClick={gc.leaveGroupCall}><PhoneOff /></Control>
|
||||
<Control label={t('meeting.leave')} danger onClick={gc.leaveGroupCall}><PhoneOff /></Control>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoStage({ gc, participants }: { gc: ReturnType<typeof useGroupCallContext>; participants: any[] }) {
|
||||
const { t } = useI18n()
|
||||
const user = useStore(s => s.user)
|
||||
const tiles = [{ peerId: user?.id || 'local', nickname: `${user?.nickname || '我'}(我)`, stream: gc.localStream,
|
||||
const tiles = [{ peerId: user?.id || 'local', nickname: `${user?.nickname || t('meeting.me')} (${t('meeting.me')})`, stream: gc.localStream,
|
||||
isMuted: gc.isMuted, isCameraOff: gc.isCameraOff, isHost: gc.isHost }, ...participants]
|
||||
const visible = gc.meetingMode === 'lecture'
|
||||
? [...tiles].sort((a, b) => Number(b.isHost) - Number(a.isHost)).slice(0, 6)
|
||||
@@ -76,33 +79,35 @@ function VideoStage({ gc, participants }: { gc: ReturnType<typeof useGroupCallCo
|
||||
<div style={{ height: '100%', display: 'grid', gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
||||
gridAutoRows: 'minmax(0, 1fr)', gap: 6, overflow: 'hidden' }}>
|
||||
{visible.map(p => <ParticipantTile key={p.peerId} participant={p} />)}
|
||||
{tiles.length > visible.length && <div style={moreTile}><LayoutGrid size={30} /><span>另有 {tiles.length - visible.length} 人</span></div>}
|
||||
{tiles.length > visible.length && <div style={moreTile}><LayoutGrid size={30} /><span>{t('meeting.more_people')} {tiles.length - visible.length}</span></div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ParticipantTile({ participant: p }: { participant: any }) {
|
||||
const { t } = useI18n()
|
||||
const ref = useRef<HTMLVideoElement>(null)
|
||||
useEffect(() => { if (ref.current) ref.current.srcObject = p.stream }, [p.stream])
|
||||
const hasVideo = p.stream?.getVideoTracks().some((t: MediaStreamTrack) => t.enabled && t.readyState === 'live') && !p.isCameraOff
|
||||
return (
|
||||
<article style={{ position: 'relative', minHeight: 0, overflow: 'hidden', borderRadius: 10, background: '#151a22',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', border: p.isSpeaking ? '2px solid #38bdf8' : '2px solid transparent' }}>
|
||||
<video ref={ref} autoPlay playsInline muted={p.nickname?.endsWith('(我)')} style={{ width: '100%', height: '100%', objectFit: 'cover', display: hasVideo ? 'block' : 'none' }} />
|
||||
<video ref={ref} autoPlay playsInline muted={p.peerId === useStore.getState().user?.id} style={{ width: '100%', height: '100%', objectFit: 'cover', display: hasVideo ? 'block' : 'none' }} />
|
||||
{!hasVideo && <Avatar name={p.nickname} src={p.avatar} size={64} />}
|
||||
<span style={nameplate}>{p.nickname || p.peerId}{p.isHost && ' · 主席'} {p.isMuted && <MicOff size={12} />}</span>
|
||||
<span style={nameplate}>{p.nickname || p.peerId}{p.isHost && ` · ${t('meeting.host')}`} {p.isMuted && <MicOff size={12} />}</span>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
function PeoplePanel({ gc, participants }: { gc: ReturnType<typeof useGroupCallContext>; participants: any[] }) {
|
||||
const { t } = useI18n()
|
||||
const user = useStore(s => s.user)
|
||||
return <aside style={{ width: 'min(320px, 42vw)', borderLeft: '1px solid #2b3442', padding: 14, overflowY: 'auto', background: '#111720' }}>
|
||||
<h3 style={{ margin: '4px 0 14px' }}>参会者({participants.length + 1})</h3>
|
||||
{[{ peerId: user?.id, nickname: `${user?.nickname || '我'}(我)`, avatar: user?.avatar, isMuted: gc.isMuted, isHost: gc.isHost }, ...participants].map(p =>
|
||||
<h3 style={{ margin: '4px 0 14px' }}>{t('meeting.participants')} ({participants.length + 1})</h3>
|
||||
{[{ peerId: user?.id, nickname: `${user?.nickname || t('meeting.me')} (${t('meeting.me')})`, avatar: user?.avatar, isMuted: gc.isMuted, isHost: gc.isHost }, ...participants].map(p =>
|
||||
<div key={p.peerId} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0' }}>
|
||||
<Avatar name={p.nickname} src={p.avatar} size={34} /><span style={{ flex: 1 }}>{p.nickname}</span>
|
||||
{p.isHost && <span style={{ fontSize: 11, color: '#65b7ff' }}>主席</span>}{p.isMuted ? <MicOff size={16} color="#a9b2c3" /> : <Mic size={16} />}
|
||||
{p.isHost && <span style={{ fontSize: 11, color: '#65b7ff' }}>{t('meeting.host')}</span>}{p.isMuted ? <MicOff size={16} color="#a9b2c3" /> : <Mic size={16} />}
|
||||
</div>)}
|
||||
</aside>
|
||||
}
|
||||
@@ -119,7 +124,7 @@ function Control({ label, children, onClick, active, danger, disabled }: any) {
|
||||
</button>
|
||||
}
|
||||
function RoundButton({ label, color, onClick, children }: any) { return <button onClick={onClick} style={{ border: 0, background: 'none', color: '#fff', display: 'grid', gap: 8, justifyItems: 'center', cursor: 'pointer' }}><span style={{ width: 64, height: 64, borderRadius: '50%', background: color, display: 'grid', placeItems: 'center' }}>{children}</span>{label}</button> }
|
||||
function MeetingError({ message, onClose }: { message: string; onClose: () => void }) { return <div style={{ position: 'fixed', left: '50%', bottom: 30, transform: 'translateX(-50%)', zIndex: 10001, padding: '12px 16px', borderRadius: 10, color: '#fff', background: '#a32931' }}>{message} <button onClick={onClose} style={{ marginLeft: 12 }}>关闭</button></div> }
|
||||
function MeetingError({ message, onClose }: { message: string; onClose: () => void }) { const { t } = useI18n(); return <div style={{ position: 'fixed', left: '50%', bottom: 30, transform: 'translateX(-50%)', zIndex: 10001, padding: '12px 16px', borderRadius: 10, color: '#fff', background: '#a32931' }}>{message} <button onClick={onClose} style={{ marginLeft: 12 }}>{t('meeting.close')}</button></div> }
|
||||
|
||||
const shellStyle: React.CSSProperties = { position: 'fixed', inset: 0, zIndex: 10000, display: 'flex', flexDirection: 'column', background: '#0b1017', color: '#fff' }
|
||||
const headerStyle: React.CSSProperties = { minHeight: 58, padding: 'calc(8px + env(safe-area-inset-top)) 12px 8px', display: 'flex', alignItems: 'center', gap: 10, background: '#111720', borderBottom: '1px solid #2b3442' }
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { post } from '../api/http'
|
||||
import { onWs, sendWs } from '../api/socket'
|
||||
import { playCallRingtone, showBrowserNotification, stopRingtone } from '../utils/notification'
|
||||
import { useI18n } from './useI18n'
|
||||
|
||||
export type GroupCallStatus = 'idle' | 'ringing' | 'connecting' | 'connected'
|
||||
export type MeetingMode = 'discussion' | 'lecture'
|
||||
@@ -30,6 +31,7 @@ const decoder = new TextDecoder()
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
export function useGroupCall(userId: string | undefined) {
|
||||
const { t } = useI18n()
|
||||
const [status, setStatus] = useState<GroupCallStatus>('idle')
|
||||
const [callId, setCallId] = useState<string | null>(null)
|
||||
const [groupId, setGroupId] = useState<string | null>(null)
|
||||
@@ -172,9 +174,9 @@ export function useGroupCall(userId: string | undefined) {
|
||||
sendWs({ type: 'group_call_invite', group_id: gid, call_id: cid, is_video: video })
|
||||
} catch (e) {
|
||||
cleanup()
|
||||
setError(e instanceof Error ? e.message : '无法连接会议服务器')
|
||||
setError(e instanceof Error ? e.message : t('meeting.connection_failed'))
|
||||
}
|
||||
}, [cleanup, connectMeeting])
|
||||
}, [cleanup, connectMeeting, t])
|
||||
|
||||
const acceptGroupCall = useCallback(async () => {
|
||||
if (!groupIdRef.current || !callIdRef.current) return
|
||||
@@ -184,9 +186,9 @@ export function useGroupCall(userId: string | undefined) {
|
||||
sendWs({ type: 'group_call_join', group_id: groupIdRef.current, call_id: callIdRef.current })
|
||||
} catch (e) {
|
||||
cleanup()
|
||||
setError(e instanceof Error ? e.message : '无法连接会议服务器')
|
||||
setError(e instanceof Error ? e.message : t('meeting.connection_failed'))
|
||||
}
|
||||
}, [cleanup, connectMeeting])
|
||||
}, [cleanup, connectMeeting, t])
|
||||
|
||||
const leaveGroupCall = useCallback(() => {
|
||||
if (groupIdRef.current && callIdRef.current) sendWs({ type: 'group_call_leave', group_id: groupIdRef.current, call_id: callIdRef.current })
|
||||
@@ -223,10 +225,10 @@ export function useGroupCall(userId: string | undefined) {
|
||||
setCallId(data.call_id); setGroupId(data.group_id); setIsVideo(!!data.is_video)
|
||||
setInviterName(data.from_nickname || data.from || ''); setInviterAvatar(data.from_avatar || '')
|
||||
setGroupName(data.group_name || ''); setStatus('ringing'); playCallRingtone()
|
||||
showBrowserNotification('PaperPhonePlus', data.is_video ? '群视频会议' : '群语音会议', () => window.focus())
|
||||
showBrowserNotification('PaperPhonePlus', t(data.is_video ? 'meeting.video_title' : 'meeting.voice_title'), () => window.focus())
|
||||
})
|
||||
return off
|
||||
}, [userId])
|
||||
}, [t, userId])
|
||||
|
||||
useEffect(() => () => cleanup(), [cleanup])
|
||||
|
||||
|
||||
@@ -132,4 +132,9 @@ export const de: Record<string, string> = {
|
||||
'profile.delete_confirm': 'Konto löschen',
|
||||
'profile.delete_enter_password': 'Passwort zur Bestätigung eingeben',
|
||||
'profile.delete_need_password': 'Bitte geben Sie Ihr Passwort ein',
|
||||
'chat.ws_disconnected': 'Verbindung unterbrochen, Wiederverbindung...', 'group.name_hint': 'Gruppennamen eingeben', 'profile.push_blocked': 'Benachrichtigungen sind blockiert. Bitte aktivieren Sie sie in den Browsereinstellungen.', 'profile.push_failed': 'Benachrichtigungen konnten nicht aktiviert werden. Details finden Sie in der Konsole.', 'call.connecting': 'Verbindung wird hergestellt...',
|
||||
'terms.title': 'Nutzungsbedingungen', 'terms.agree_prefix': 'Ich habe die folgenden Bedingungen gelesen und stimme ihnen zu:', 'terms.agree_link': 'Nutzungsbedingungen', 'terms.must_agree': 'Sie müssen den Nutzungsbedingungen zustimmen, um fortzufahren', 'terms.hero_title': 'Nutzungsbedingungen', 'terms.hero_subtitle': 'Bitte lesen Sie diese Bedingungen sorgfältig, bevor Sie PaperPhonePlus verwenden',
|
||||
'report.title': 'Inhalt melden', 'report.reason': 'Grund der Meldung', 'report.reason_offensive': 'Anstößiger oder unangemessener Inhalt', 'report.reason_spam': 'Spam oder Werbung', 'report.reason_harassment': 'Belästigung oder Mobbing', 'report.reason_violence': 'Gewalt oder Drohungen', 'report.reason_misinformation': 'Falschinformationen', 'report.reason_other': 'Sonstiges', 'report.detail_placeholder': 'Weitere Details (optional)', 'report.submit': 'Meldung absenden', 'report.success': 'Meldung übermittelt. Wir prüfen sie innerhalb von 24 Stunden.', 'report.failed': 'Meldung konnte nicht gesendet werden. Bitte versuchen Sie es erneut.', 'report.report_post': 'Beitrag melden', 'report.report_message': 'Nachricht melden', 'report.report_user': 'Benutzer melden',
|
||||
'block.user': 'Benutzer blockieren', 'block.confirm_title': 'Diesen Benutzer blockieren?', 'block.confirm_desc': 'Blockierte Benutzer können Ihnen keine Nachrichten senden. Ihre Inhalte werden aus Ihrem Feed entfernt. Sie können die Blockierung später aufheben.', 'block.success': 'Benutzer erfolgreich blockiert', 'block.failed': 'Benutzer konnte nicht blockiert werden', 'unblock.user': 'Benutzer entsperren', 'unblock.success': 'Benutzer entsperrt', 'unblock.failed': 'Benutzer konnte nicht entsperrt werden',
|
||||
'meeting.video_title': 'Gruppen-Videokonferenz', 'meeting.voice_title': 'Gruppen-Audiokonferenz', 'meeting.video': 'Videokonferenz', 'meeting.voice': 'Audiokonferenz', 'meeting.invites_you': 'lädt Sie ein zu einer', 'meeting.reject': 'Ablehnen', 'meeting.join': 'Beitreten', 'meeting.leave': 'Konferenz verlassen', 'meeting.joining': 'Konferenz wird betreten…', 'meeting.people_unit': 'Personen', 'meeting.lecture_mode': 'Vortragsmodus', 'meeting.discussion_mode': 'Freie Diskussion', 'meeting.mute': 'Stummschalten', 'meeting.unmute': 'Stummschaltung aufheben', 'meeting.start_video': 'Video starten', 'meeting.stop_video': 'Video beenden', 'meeting.mute_all': 'Alle stummschalten', 'meeting.me': 'Ich', 'meeting.more_people': 'Weitere Teilnehmer:', 'meeting.host': 'Moderator', 'meeting.participants': 'Teilnehmer', 'meeting.close': 'Schließen', 'meeting.connection_failed': 'Verbindung zum Konferenzserver nicht möglich',
|
||||
}
|
||||
|
||||
@@ -436,4 +436,29 @@ export const en: Record<string, string> = {
|
||||
'unblock.user': 'Unblock User',
|
||||
'unblock.success': 'User unblocked',
|
||||
'unblock.failed': 'Failed to unblock user',
|
||||
|
||||
// Video meeting
|
||||
'meeting.video_title': 'Group Video Meeting',
|
||||
'meeting.voice_title': 'Group Voice Meeting',
|
||||
'meeting.video': 'video meeting',
|
||||
'meeting.voice': 'voice meeting',
|
||||
'meeting.invites_you': 'invites you to join a',
|
||||
'meeting.reject': 'Reject',
|
||||
'meeting.join': 'Join',
|
||||
'meeting.leave': 'Leave',
|
||||
'meeting.joining': 'Joining meeting…',
|
||||
'meeting.people_unit': 'people',
|
||||
'meeting.lecture_mode': 'Lecture mode',
|
||||
'meeting.discussion_mode': 'Open discussion',
|
||||
'meeting.mute': 'Mute',
|
||||
'meeting.unmute': 'Unmute',
|
||||
'meeting.start_video': 'Start video',
|
||||
'meeting.stop_video': 'Stop video',
|
||||
'meeting.mute_all': 'Mute all',
|
||||
'meeting.me': 'Me',
|
||||
'meeting.more_people': 'More participants:',
|
||||
'meeting.host': 'Host',
|
||||
'meeting.participants': 'Participants',
|
||||
'meeting.close': 'Close',
|
||||
'meeting.connection_failed': 'Unable to connect to the meeting server',
|
||||
}
|
||||
|
||||
@@ -132,4 +132,9 @@ export const es: Record<string, string> = {
|
||||
'profile.delete_confirm': 'Eliminar cuenta',
|
||||
'profile.delete_enter_password': 'Ingresa tu contraseña para confirmar',
|
||||
'profile.delete_need_password': 'Por favor ingresa tu contraseña',
|
||||
'chat.ws_disconnected': 'Conexión perdida, reconectando...', 'group.name_hint': 'Ingresa el nombre del grupo', 'profile.push_blocked': 'Las notificaciones están bloqueadas. Actívalas en la configuración del navegador.', 'profile.push_failed': 'No se pudieron activar las notificaciones. Consulta la consola para más detalles.', 'call.connecting': 'Conectando...',
|
||||
'terms.title': 'Términos de uso', 'terms.agree_prefix': 'He leído y acepto los', 'terms.agree_link': 'Términos de uso', 'terms.must_agree': 'Debes aceptar los Términos de uso para continuar', 'terms.hero_title': 'Términos de uso', 'terms.hero_subtitle': 'Lee atentamente estos términos antes de usar PaperPhonePlus',
|
||||
'report.title': 'Reportar contenido', 'report.reason': 'Motivo del reporte', 'report.reason_offensive': 'Contenido ofensivo o inapropiado', 'report.reason_spam': 'Spam o publicidad', 'report.reason_harassment': 'Acoso o intimidación', 'report.reason_violence': 'Violencia o amenazas', 'report.reason_misinformation': 'Información falsa', 'report.reason_other': 'Otro', 'report.detail_placeholder': 'Detalles adicionales (opcional)', 'report.submit': 'Enviar reporte', 'report.success': 'Reporte enviado. Lo revisaremos en un plazo de 24 horas.', 'report.failed': 'No se pudo enviar el reporte. Inténtalo de nuevo.', 'report.report_post': 'Reportar publicación', 'report.report_message': 'Reportar mensaje', 'report.report_user': 'Reportar usuario',
|
||||
'block.user': 'Bloquear usuario', 'block.confirm_title': '¿Bloquear a este usuario?', 'block.confirm_desc': 'Los usuarios bloqueados no podrán enviarte mensajes. Su contenido se eliminará de tu feed. Podrás desbloquearlos más tarde.', 'block.success': 'Usuario bloqueado', 'block.failed': 'No se pudo bloquear al usuario', 'unblock.user': 'Desbloquear usuario', 'unblock.success': 'Usuario desbloqueado', 'unblock.failed': 'No se pudo desbloquear al usuario',
|
||||
'meeting.video_title': 'Videoconferencia grupal', 'meeting.voice_title': 'Conferencia de voz grupal', 'meeting.video': 'videoconferencia', 'meeting.voice': 'conferencia de voz', 'meeting.invites_you': 'te invita a una', 'meeting.reject': 'Rechazar', 'meeting.join': 'Unirse', 'meeting.leave': 'Salir de la reunión', 'meeting.joining': 'Entrando a la reunión…', 'meeting.people_unit': 'personas', 'meeting.lecture_mode': 'Modo presentación', 'meeting.discussion_mode': 'Debate libre', 'meeting.mute': 'Silenciar', 'meeting.unmute': 'Activar micrófono', 'meeting.start_video': 'Iniciar vídeo', 'meeting.stop_video': 'Detener vídeo', 'meeting.mute_all': 'Silenciar a todos', 'meeting.me': 'Yo', 'meeting.more_people': 'Más participantes:', 'meeting.host': 'Anfitrión', 'meeting.participants': 'Participantes', 'meeting.close': 'Cerrar', 'meeting.connection_failed': 'No se puede conectar al servidor de reuniones',
|
||||
}
|
||||
|
||||
@@ -132,4 +132,9 @@ export const fr: Record<string, string> = {
|
||||
'profile.delete_confirm': 'Supprimer le compte',
|
||||
'profile.delete_enter_password': 'Entrez votre mot de passe pour confirmer',
|
||||
'profile.delete_need_password': 'Veuillez entrer votre mot de passe',
|
||||
'chat.ws_disconnected': 'Connexion perdue, reconnexion...', 'group.name_hint': 'Saisissez le nom du groupe', 'profile.push_blocked': 'Les notifications sont bloquées. Activez-les dans les paramètres du navigateur.', 'profile.push_failed': 'Impossible d\'activer les notifications. Consultez la console pour plus de détails.', 'call.connecting': 'Connexion...',
|
||||
'terms.title': 'Conditions d\'utilisation', 'terms.agree_prefix': 'J\'ai lu et j\'accepte les', 'terms.agree_link': 'Conditions d\'utilisation', 'terms.must_agree': 'Vous devez accepter les conditions d\'utilisation pour continuer', 'terms.hero_title': 'Conditions d\'utilisation', 'terms.hero_subtitle': 'Veuillez lire attentivement ces conditions avant d\'utiliser PaperPhonePlus',
|
||||
'report.title': 'Signaler le contenu', 'report.reason': 'Motif du signalement', 'report.reason_offensive': 'Contenu offensant ou inapproprié', 'report.reason_spam': 'Spam ou publicité', 'report.reason_harassment': 'Harcèlement ou intimidation', 'report.reason_violence': 'Violence ou menaces', 'report.reason_misinformation': 'Désinformation', 'report.reason_other': 'Autre', 'report.detail_placeholder': 'Détails supplémentaires (facultatif)', 'report.submit': 'Envoyer le signalement', 'report.success': 'Signalement envoyé. Nous l\'examinerons sous 24 heures.', 'report.failed': 'Échec de l\'envoi du signalement. Veuillez réessayer.', 'report.report_post': 'Signaler la publication', 'report.report_message': 'Signaler le message', 'report.report_user': 'Signaler l\'utilisateur',
|
||||
'block.user': 'Bloquer l\'utilisateur', 'block.confirm_title': 'Bloquer cet utilisateur ?', 'block.confirm_desc': 'Les utilisateurs bloqués ne peuvent plus vous envoyer de messages. Leur contenu sera retiré de votre fil. Vous pourrez les débloquer plus tard.', 'block.success': 'Utilisateur bloqué', 'block.failed': 'Échec du blocage de l\'utilisateur', 'unblock.user': 'Débloquer l\'utilisateur', 'unblock.success': 'Utilisateur débloqué', 'unblock.failed': 'Échec du déblocage de l\'utilisateur',
|
||||
'meeting.video_title': 'Visioconférence de groupe', 'meeting.voice_title': 'Conférence audio de groupe', 'meeting.video': 'visioconférence', 'meeting.voice': 'conférence audio', 'meeting.invites_you': 'vous invite à une', 'meeting.reject': 'Refuser', 'meeting.join': 'Rejoindre', 'meeting.leave': 'Quitter la réunion', 'meeting.joining': 'Connexion à la réunion…', 'meeting.people_unit': 'personnes', 'meeting.lecture_mode': 'Mode présentation', 'meeting.discussion_mode': 'Discussion libre', 'meeting.mute': 'Couper le micro', 'meeting.unmute': 'Réactiver le micro', 'meeting.start_video': 'Activer la vidéo', 'meeting.stop_video': 'Désactiver la vidéo', 'meeting.mute_all': 'Couper tous les micros', 'meeting.me': 'Moi', 'meeting.more_people': 'Autres participants :', 'meeting.host': 'Animateur', 'meeting.participants': 'Participants', 'meeting.close': 'Fermer', 'meeting.connection_failed': 'Impossible de se connecter au serveur de réunion',
|
||||
}
|
||||
|
||||
@@ -129,4 +129,10 @@ export const ja: Record<string, string> = {
|
||||
'profile.delete_confirm': 'アカウントを削除',
|
||||
'profile.delete_enter_password': 'パスワードを入力して確認',
|
||||
'profile.delete_need_password': 'パスワードを入力してください',
|
||||
'chat.ws_disconnected': '接続が切断されました。再接続中...', 'group.name_hint': 'グループ名を入力',
|
||||
'profile.push_blocked': '通知がブロックされています。ブラウザの設定で有効にしてください。', 'profile.push_failed': '通知を有効にできませんでした。詳細はコンソールを確認してください。', 'call.connecting': '接続中...',
|
||||
'terms.title': '利用規約', 'terms.agree_prefix': '以下を読み、同意します:', 'terms.agree_link': '利用規約', 'terms.must_agree': '続行するには利用規約への同意が必要です', 'terms.hero_title': '利用規約', 'terms.hero_subtitle': 'PaperPhonePlusを利用する前に、この規約をよくお読みください',
|
||||
'report.title': 'コンテンツを通報', 'report.reason': '通報理由', 'report.reason_offensive': '攻撃的または不適切なコンテンツ', 'report.reason_spam': 'スパムまたは広告', 'report.reason_harassment': '嫌がらせまたはいじめ', 'report.reason_violence': '暴力または脅迫', 'report.reason_misinformation': '誤情報', 'report.reason_other': 'その他', 'report.detail_placeholder': '追加詳細(任意)', 'report.submit': '通報を送信', 'report.success': '通報を送信しました。24時間以内に確認します。', 'report.failed': '通報の送信に失敗しました。もう一度お試しください。', 'report.report_post': '投稿を通報', 'report.report_message': 'メッセージを通報', 'report.report_user': 'ユーザーを通報',
|
||||
'block.user': 'ユーザーをブロック', 'block.confirm_title': 'このユーザーをブロックしますか?', 'block.confirm_desc': 'ブロックしたユーザーはあなたにメッセージを送信できず、そのコンテンツはフィードから削除されます。後で解除できます。', 'block.success': 'ユーザーをブロックしました', 'block.failed': 'ユーザーのブロックに失敗しました', 'unblock.user': 'ブロックを解除', 'unblock.success': 'ブロックを解除しました', 'unblock.failed': 'ブロック解除に失敗しました',
|
||||
'meeting.video_title': 'グループビデオ会議', 'meeting.voice_title': 'グループ音声会議', 'meeting.video': 'ビデオ会議', 'meeting.voice': '音声会議', 'meeting.invites_you': 'があなたを招待しています:', 'meeting.reject': '拒否', 'meeting.join': '参加', 'meeting.leave': '会議を退出', 'meeting.joining': '会議に参加中…', 'meeting.people_unit': '人', 'meeting.lecture_mode': '講義モード', 'meeting.discussion_mode': '自由討論', 'meeting.mute': 'ミュート', 'meeting.unmute': 'ミュート解除', 'meeting.start_video': 'ビデオを開始', 'meeting.stop_video': 'ビデオを停止', 'meeting.mute_all': '全員をミュート', 'meeting.me': '自分', 'meeting.more_people': 'その他の参加者:', 'meeting.host': 'ホスト', 'meeting.participants': '参加者', 'meeting.close': '閉じる', 'meeting.connection_failed': '会議サーバーに接続できません',
|
||||
}
|
||||
|
||||
@@ -130,4 +130,9 @@ export const ko: Record<string, string> = {
|
||||
'profile.delete_confirm': '계정 삭제',
|
||||
'profile.delete_enter_password': '비밀번호를 입력하여 확인',
|
||||
'profile.delete_need_password': '비밀번호를 입력해 주세요',
|
||||
'chat.ws_disconnected': '연결이 끊어져 다시 연결 중...', 'group.name_hint': '그룹 이름 입력', 'profile.push_blocked': '알림이 차단되었습니다. 브라우저 설정에서 활성화하세요.', 'profile.push_failed': '알림을 활성화하지 못했습니다. 자세한 내용은 콘솔을 확인하세요.', 'call.connecting': '연결 중...',
|
||||
'terms.title': '이용약관', 'terms.agree_prefix': '다음을 읽고 동의합니다:', 'terms.agree_link': '이용약관', 'terms.must_agree': '계속하려면 이용약관에 동의해야 합니다', 'terms.hero_title': '이용약관', 'terms.hero_subtitle': 'PaperPhonePlus를 사용하기 전에 약관을 자세히 읽어 주세요',
|
||||
'report.title': '콘텐츠 신고', 'report.reason': '신고 사유', 'report.reason_offensive': '모욕적이거나 부적절한 콘텐츠', 'report.reason_spam': '스팸 또는 광고', 'report.reason_harassment': '괴롭힘 또는 따돌림', 'report.reason_violence': '폭력 또는 위협', 'report.reason_misinformation': '허위 정보', 'report.reason_other': '기타', 'report.detail_placeholder': '추가 세부 사항(선택)', 'report.submit': '신고 제출', 'report.success': '신고가 제출되었습니다. 24시간 이내에 검토하겠습니다.', 'report.failed': '신고를 제출하지 못했습니다. 다시 시도하세요.', 'report.report_post': '게시물 신고', 'report.report_message': '메시지 신고', 'report.report_user': '사용자 신고',
|
||||
'block.user': '사용자 차단', 'block.confirm_title': '이 사용자를 차단할까요?', 'block.confirm_desc': '차단된 사용자는 메시지를 보낼 수 없고 피드에서 해당 콘텐츠가 제거됩니다. 나중에 차단을 해제할 수 있습니다.', 'block.success': '사용자를 차단했습니다', 'block.failed': '사용자 차단에 실패했습니다', 'unblock.user': '사용자 차단 해제', 'unblock.success': '차단을 해제했습니다', 'unblock.failed': '차단 해제에 실패했습니다',
|
||||
'meeting.video_title': '그룹 화상 회의', 'meeting.voice_title': '그룹 음성 회의', 'meeting.video': '화상 회의', 'meeting.voice': '음성 회의', 'meeting.invites_you': '님이 당신을 초대했습니다:', 'meeting.reject': '거절', 'meeting.join': '참가', 'meeting.leave': '회의 나가기', 'meeting.joining': '회의 참가 중…', 'meeting.people_unit': '명', 'meeting.lecture_mode': '강의 모드', 'meeting.discussion_mode': '자유 토론', 'meeting.mute': '음소거', 'meeting.unmute': '음소거 해제', 'meeting.start_video': '비디오 시작', 'meeting.stop_video': '비디오 중지', 'meeting.mute_all': '전체 음소거', 'meeting.me': '나', 'meeting.more_people': '추가 참가자:', 'meeting.host': '호스트', 'meeting.participants': '참가자', 'meeting.close': '닫기', 'meeting.connection_failed': '회의 서버에 연결할 수 없습니다',
|
||||
}
|
||||
|
||||
@@ -132,4 +132,9 @@ export const ru: Record<string, string> = {
|
||||
'profile.delete_confirm': 'Удалить аккаунт',
|
||||
'profile.delete_enter_password': 'Введите пароль для подтверждения',
|
||||
'profile.delete_need_password': 'Пожалуйста, введите пароль',
|
||||
'chat.ws_disconnected': 'Соединение потеряно, переподключение...', 'group.name_hint': 'Введите название группы', 'profile.push_blocked': 'Уведомления заблокированы. Разрешите их в настройках браузера.', 'profile.push_failed': 'Не удалось включить уведомления. Подробности доступны в консоли.', 'call.connecting': 'Подключение...',
|
||||
'terms.title': 'Условия использования', 'terms.agree_prefix': 'Я прочитал(-а) и принимаю', 'terms.agree_link': 'Условия использования', 'terms.must_agree': 'Для продолжения необходимо принять Условия использования', 'terms.hero_title': 'Условия использования', 'terms.hero_subtitle': 'Внимательно прочитайте эти условия перед использованием PaperPhonePlus',
|
||||
'report.title': 'Пожаловаться на контент', 'report.reason': 'Причина жалобы', 'report.reason_offensive': 'Оскорбительный или неприемлемый контент', 'report.reason_spam': 'Спам или реклама', 'report.reason_harassment': 'Преследование или травля', 'report.reason_violence': 'Насилие или угрозы', 'report.reason_misinformation': 'Недостоверная информация', 'report.reason_other': 'Другое', 'report.detail_placeholder': 'Дополнительные сведения (необязательно)', 'report.submit': 'Отправить жалобу', 'report.success': 'Жалоба отправлена. Мы рассмотрим её в течение 24 часов.', 'report.failed': 'Не удалось отправить жалобу. Повторите попытку.', 'report.report_post': 'Пожаловаться на публикацию', 'report.report_message': 'Пожаловаться на сообщение', 'report.report_user': 'Пожаловаться на пользователя',
|
||||
'block.user': 'Заблокировать пользователя', 'block.confirm_title': 'Заблокировать этого пользователя?', 'block.confirm_desc': 'Заблокированные пользователи не смогут отправлять вам сообщения. Их контент будет удалён из вашей ленты. Позже блокировку можно снять.', 'block.success': 'Пользователь заблокирован', 'block.failed': 'Не удалось заблокировать пользователя', 'unblock.user': 'Разблокировать пользователя', 'unblock.success': 'Пользователь разблокирован', 'unblock.failed': 'Не удалось разблокировать пользователя',
|
||||
'meeting.video_title': 'Групповая видеоконференция', 'meeting.voice_title': 'Групповая аудиоконференция', 'meeting.video': 'видеоконференцию', 'meeting.voice': 'аудиоконференцию', 'meeting.invites_you': 'приглашает вас на', 'meeting.reject': 'Отклонить', 'meeting.join': 'Присоединиться', 'meeting.leave': 'Покинуть конференцию', 'meeting.joining': 'Подключение к конференции…', 'meeting.people_unit': 'участников', 'meeting.lecture_mode': 'Режим лекции', 'meeting.discussion_mode': 'Свободное обсуждение', 'meeting.mute': 'Выключить микрофон', 'meeting.unmute': 'Включить микрофон', 'meeting.start_video': 'Включить видео', 'meeting.stop_video': 'Выключить видео', 'meeting.mute_all': 'Выключить микрофоны всем', 'meeting.me': 'Я', 'meeting.more_people': 'Другие участники:', 'meeting.host': 'Ведущий', 'meeting.participants': 'Участники', 'meeting.close': 'Закрыть', 'meeting.connection_failed': 'Не удалось подключиться к серверу конференций',
|
||||
}
|
||||
|
||||
@@ -450,4 +450,29 @@ export const zh: Record<string, string> = {
|
||||
'unblock.user': '取消拉黑',
|
||||
'unblock.success': '已取消拉黑',
|
||||
'unblock.failed': '取消拉黑失败',
|
||||
|
||||
// 视频会议
|
||||
'meeting.video_title': '群视频会议',
|
||||
'meeting.voice_title': '群语音会议',
|
||||
'meeting.video': '视频会议',
|
||||
'meeting.voice': '语音会议',
|
||||
'meeting.invites_you': '邀请你加入',
|
||||
'meeting.reject': '拒绝',
|
||||
'meeting.join': '加入',
|
||||
'meeting.leave': '离开会议',
|
||||
'meeting.joining': '正在进入会议…',
|
||||
'meeting.people_unit': '人',
|
||||
'meeting.lecture_mode': '讲课模式',
|
||||
'meeting.discussion_mode': '自由讨论',
|
||||
'meeting.mute': '静音',
|
||||
'meeting.unmute': '解除静音',
|
||||
'meeting.start_video': '开启视频',
|
||||
'meeting.stop_video': '关闭视频',
|
||||
'meeting.mute_all': '全员静音',
|
||||
'meeting.me': '我',
|
||||
'meeting.more_people': '其他参会者:',
|
||||
'meeting.host': '主席',
|
||||
'meeting.participants': '参会者',
|
||||
'meeting.close': '关闭',
|
||||
'meeting.connection_failed': '无法连接会议服务器',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user