add account localization and switchable rooms
Centralize control and OBS copy in a shared TOML catalog, persist the selected locale per account, and broadcast language changes to component streams. Allow account owners to atomically switch their Bilibili room and restart the shared listener without changing component URLs.
This commit is contained in:
+30
-6
@@ -21,22 +21,26 @@ import type {
|
||||
} from './types'
|
||||
import { normalizeThemeId } from './themes'
|
||||
import { defaultOverlaySettings, defaultSongRequestSettings } from './types'
|
||||
import { currentLanguage, hasTranslation, translate } from './i18n'
|
||||
|
||||
export class ApiError extends Error {
|
||||
readonly status: number
|
||||
readonly code?: string
|
||||
readonly messageKey?: string
|
||||
readonly fieldErrors?: Record<string, string>
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
message: string,
|
||||
code?: string,
|
||||
messageKey?: string,
|
||||
fieldErrors?: Record<string, string>,
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.status = status
|
||||
this.code = code
|
||||
this.messageKey = messageKey
|
||||
this.fieldErrors = fieldErrors
|
||||
}
|
||||
}
|
||||
@@ -52,12 +56,13 @@ async function parseResponse(response: Response): Promise<unknown> {
|
||||
export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const method = (init.method ?? 'GET').toUpperCase()
|
||||
if (!navigator.onLine && !['GET', 'HEAD'].includes(method)) {
|
||||
throw new ApiError(0, '当前处于离线状态,操作没有提交;联网后请重试。', 'offline')
|
||||
throw new ApiError(0, translate('api.offline'), 'offline', 'api.error.offline')
|
||||
}
|
||||
const headers = new Headers(init.headers)
|
||||
if (init.body != null && !headers.has('content-type'))
|
||||
headers.set('content-type', 'application/json')
|
||||
headers.set('accept', 'application/json')
|
||||
headers.set('accept-language', currentLanguage())
|
||||
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
@@ -70,10 +75,24 @@ export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
window.dispatchEvent(new Event('lxc:session-expired'))
|
||||
}
|
||||
const error = payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
|
||||
const code = typeof error.code === 'string' ? error.code : undefined
|
||||
const messageKey =
|
||||
typeof error.messageKey === 'string'
|
||||
? error.messageKey
|
||||
: typeof error.message_key === 'string'
|
||||
? error.message_key
|
||||
: code
|
||||
? `api.error.${code}`
|
||||
: undefined
|
||||
const localized = messageKey && hasTranslation(messageKey) ? translate(messageKey) : undefined
|
||||
throw new ApiError(
|
||||
response.status,
|
||||
String(error.message ?? error.error ?? `请求失败(HTTP ${response.status})`),
|
||||
typeof error.code === 'string' ? error.code : undefined,
|
||||
localized ??
|
||||
String(
|
||||
error.message ?? error.error ?? translate('api.http_error', { status: response.status }),
|
||||
),
|
||||
code,
|
||||
messageKey,
|
||||
error.fieldErrors && typeof error.fieldErrors === 'object'
|
||||
? (error.fieldErrors as Record<string, string>)
|
||||
: undefined,
|
||||
@@ -166,7 +185,7 @@ export function normalizeComponents(value: unknown): ComponentSummary[] {
|
||||
id: String(item.id ?? ''),
|
||||
publicId: String(item.publicId ?? item.public_id ?? item.id ?? ''),
|
||||
kind: String(item.kind ?? item.type ?? 'danmaku'),
|
||||
name: String(item.name ?? '弹幕姬'),
|
||||
name: String(item.name ?? translate('components.danmaku_type')),
|
||||
enabled: typeof item.enabled === 'boolean' ? item.enabled : undefined,
|
||||
settings: item.settings as OverlaySettings | undefined,
|
||||
updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : undefined,
|
||||
@@ -204,7 +223,10 @@ export function normalizeSongRequestItem(value: unknown): SongRequestItem | unde
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
requester: { uid: String(requester.uid ?? ''), name: String(requester.name ?? '直播间观众') },
|
||||
requester: {
|
||||
uid: String(requester.uid ?? ''),
|
||||
name: String(requester.name ?? translate('common.viewer')),
|
||||
},
|
||||
status: status as SongRequestItem['status'],
|
||||
queuePosition: Number(item.queuePosition ?? 0),
|
||||
requestedAt: String(item.requestedAt ?? ''),
|
||||
@@ -308,7 +330,9 @@ export function normalizeInvitations(value: unknown): Invitation[] {
|
||||
.filter(item => item.id)
|
||||
}
|
||||
|
||||
export function errorMessage(error: unknown, fallback = '操作失败,请稍后再试'): string {
|
||||
export function errorMessage(error: unknown, fallback = translate('common.failed')): string {
|
||||
if (error instanceof ApiError && error.messageKey && hasTranslation(error.messageKey))
|
||||
return translate(error.messageKey)
|
||||
return error instanceof Error && error.message ? error.message : fallback
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user