375 lines
16 KiB
TypeScript
375 lines
16 KiB
TypeScript
export type Locale = "zh-CN" | "en-US";
|
|
|
|
type MessageParams = Record<string, string | number>;
|
|
|
|
const DEFAULT_LOCALE: Locale = "zh-CN";
|
|
export const LOCALE_STORAGE_KEY = "polyweather.locale";
|
|
|
|
const MESSAGES: Record<Locale, Record<string, string>> = {
|
|
"zh-CN": {
|
|
"header.subtitle": "天气衍生品智能分析",
|
|
"header.home": "主页",
|
|
"header.homeAria": "返回主地图主页",
|
|
"header.docs": "文档",
|
|
"header.docsAria": "打开产品文档中心",
|
|
"header.probabilityHub": "概率页",
|
|
"header.probabilityHubAria": "打开 52 城市概率汇总页",
|
|
"header.info": "技术说明",
|
|
"header.infoAria": "查看系统技术说明",
|
|
"header.account": "账户",
|
|
"header.accountAria": "打开账户页",
|
|
"header.signIn": "登录 / 注册",
|
|
"header.signInAria": "打开登录注册页",
|
|
"header.live": "实时",
|
|
"header.refreshAria": "刷新所有数据",
|
|
"header.langAria": "切换语言",
|
|
"header.langZh": "中文",
|
|
"header.langEn": "EN",
|
|
|
|
"sidebar.title": "监控城市",
|
|
"sidebar.currentTemp": "当前 {temp}",
|
|
"sidebar.peakTempAt": "峰值 {temp} @ {time}",
|
|
"sidebar.peakAt": "峰值 @ {time}",
|
|
"sidebar.group.high": "近期强势",
|
|
"sidebar.group.medium": "近期一般",
|
|
"sidebar.group.low": "近期偏弱",
|
|
"sidebar.group.other": "样本不足",
|
|
|
|
"dashboard.loading": "正在同步站点观测与结算站点信息,请稍候...",
|
|
|
|
"detail.closeAria": "关闭城市详情面板",
|
|
"detail.waitSelect": "等待选择城市",
|
|
"detail.todayAnalysis": "今日日内分析",
|
|
"detail.history": "历史对账",
|
|
"detail.loading": "正在加载城市详情...",
|
|
"detail.emptyHint": "从左侧城市列表选择一个城市查看详情。",
|
|
"detail.sceneryAlt": "{city} 风景照",
|
|
"detail.sceneryTitle": "城市风景与微气候",
|
|
"detail.sceneryFallback":
|
|
"当前没有匹配到风景图,可从下方城市档案查看站点与观测结构。",
|
|
"detail.profile": "城市档案",
|
|
"detail.todayMiniTrend": "今日日内走势(简版)",
|
|
"detail.chartLegendEmpty": "暂无小时级实测或预测曲线。",
|
|
|
|
"forecast.title": "多日预报",
|
|
"forecast.empty": "暂无多日预报",
|
|
"forecast.today": "今天",
|
|
|
|
"guide.title": "📎 PolyWeather 系统技术说明",
|
|
"guide.closeAria": "关闭技术说明",
|
|
"guide.footer":
|
|
"数据源以 METAR、香港天文台(HKO)、NOAA 指定站点、Turkish MGM、Open-Meteo、weather.gov 为主。",
|
|
|
|
"history.title": "📊 历史准确率对账 - {city}",
|
|
"history.closeAria": "关闭历史对账",
|
|
"history.loading": "正在获取历史数据...",
|
|
"history.error": "获取历史信息失败",
|
|
"history.empty": "近 15 天暂无该城市历史数据",
|
|
"history.hitRate": "DEB 结算胜率 (WU)",
|
|
"history.mae": "DEB MAE",
|
|
"history.debHitRate": "DEB 结算胜率 (WU)",
|
|
"history.debMae": "DEB MAE",
|
|
"history.muMae": "μ MAE",
|
|
"history.bestModelMae": "最佳单模型 MAE",
|
|
"history.debVsBest": "DEB 优于最佳模型",
|
|
"history.sample": "近 15 天已结算样本",
|
|
"history.sampleDays": "{count} 天",
|
|
|
|
"future.todayTitle": "{city} · 今日日内分析",
|
|
"future.dateTitle": "{city} · {date} 未来日期分析",
|
|
"future.closeTodayAria": "关闭今日日内分析",
|
|
"future.closeDateAria": "关闭未来日期分析",
|
|
"future.currentObs": "当前实测",
|
|
"future.currentWeather": "当前天气",
|
|
"future.wuRef": "结算参考",
|
|
"future.sunrise": "日出时间",
|
|
"future.sunset": "日落时间",
|
|
"future.sunshine": "日照时长",
|
|
"future.todayForecastHigh": "今日预报高温",
|
|
"future.targetForecast": "目标日预报",
|
|
"future.deb": "DEB 预测",
|
|
"future.mu": "动态分布中心",
|
|
"future.score": "趋势评分",
|
|
"future.todayTempTrend": "今日温度走势",
|
|
"future.targetTempTrend": "目标日小时走势",
|
|
"future.probability": "模型概率参考",
|
|
"future.models": "多模型预报",
|
|
"future.structureToday": "今日日内结构信号",
|
|
"future.structureDate": "未来 6-48 小时趋势",
|
|
"future.judgement": "判断",
|
|
"future.confidence": "置信度",
|
|
"future.maxPrecip": "最大降水概率",
|
|
"future.ai": "机场报文解读",
|
|
"future.noAi": "暂无机场报文解读,当前以结构化气象与模型数据为主。",
|
|
"future.weatherGov": "weather.gov 文本",
|
|
"future.risk": "结算与偏差风险",
|
|
"future.climate": "当地气候主要受什么影响",
|
|
"future.chartLegendEmpty": "暂无机场报文或小时级实测数据",
|
|
|
|
"confidence.high": "高",
|
|
"confidence.medium": "中",
|
|
"confidence.low": "低",
|
|
|
|
"section.todayTempTrend": "今日温度走势",
|
|
"section.chartEmpty": "暂无小时级数据",
|
|
"section.probability": "校准模型概率",
|
|
"section.mu": "动态分布中心 μ = {value}{unit}",
|
|
"section.noProb": "暂无概率数据",
|
|
"section.models": "多模型预报",
|
|
"section.noModels": "暂无多模型预报",
|
|
"section.ai": "机场报文解读",
|
|
"section.aiEmpty": "暂无机场报文解读,当前以结构化气象与模型数据为主。",
|
|
"section.risk": "数据偏差风险",
|
|
"section.noRiskProfile": "暂无风险档案",
|
|
"section.airport": "机场",
|
|
"section.distance": "距离",
|
|
"section.note": "注意",
|
|
|
|
"account.title": "账户中心",
|
|
"account.subtitle": "查看身份、权限与 Bot 绑定信息",
|
|
"account.backDashboard": "返回看板",
|
|
"account.refresh": "刷新",
|
|
"account.signIn": "登录 / 注册",
|
|
"account.signOut": "退出登录",
|
|
"account.loading": "正在同步账户信息...",
|
|
"account.error": "加载失败: {message}",
|
|
"account.updatedAt": "最近同步: {time}",
|
|
"account.guestName": "PolyWeather 用户",
|
|
"account.authenticated": "已登录",
|
|
"account.guest": "游客模式",
|
|
"account.subscriptionActive": "订阅有效",
|
|
"account.subscriptionRequired": "需要订阅",
|
|
"account.subscriptionUnknown": "订阅状态未知",
|
|
"account.card.membership": "会员与权限",
|
|
"account.card.identity": "身份信息",
|
|
"account.card.backend": "后端鉴权",
|
|
"account.card.bot": "Bot 绑定",
|
|
"account.field.email": "邮箱",
|
|
"account.field.userId": "用户 ID",
|
|
"account.field.provider": "登录方式",
|
|
"account.field.lastSignIn": "最近登录",
|
|
"account.field.mode": "鉴权模式",
|
|
"account.field.backendStatus": "后端状态",
|
|
"account.field.subscription": "订阅结果",
|
|
"account.field.requirement": "订阅要求",
|
|
"account.field.bindCommand": "绑定命令",
|
|
"account.field.bindHint":
|
|
"将下面命令发送到 Telegram Bot,可把网页账户与机器人权限绑定。",
|
|
"account.mode.supabaseRequired": "Supabase 强制登录",
|
|
"account.mode.supabaseOptional": "Supabase 可选登录",
|
|
"account.mode.supabase": "Supabase 会话鉴权",
|
|
"account.mode.legacy": "Legacy Token 鉴权",
|
|
"account.mode.disabled": "未启用权限校验",
|
|
"account.mode.unknown": "未知模式",
|
|
"account.backend.ok": "通过",
|
|
"account.backend.fail": "失败",
|
|
"account.subscription.active": "有效",
|
|
"account.subscription.inactive": "无效/已过期",
|
|
"account.subscription.notRequired": "当前未强制订阅",
|
|
"account.subscription.unknown": "未知",
|
|
"account.copy": "复制",
|
|
"account.copied": "已复制",
|
|
"account.na": "--",
|
|
|
|
"common.na": "--",
|
|
},
|
|
"en-US": {
|
|
"header.subtitle": "Weather Derivatives Intelligence",
|
|
"header.home": "Home",
|
|
"header.homeAria": "Return to the main map homepage",
|
|
"header.docs": "Docs",
|
|
"header.docsAria": "Open product documentation",
|
|
"header.probabilityHub": "Probabilities",
|
|
"header.probabilityHubAria": "Open the 52-city probability hub",
|
|
"header.info": "Tech Notes",
|
|
"header.infoAria": "Open system technical notes",
|
|
"header.account": "Account",
|
|
"header.accountAria": "Open account center",
|
|
"header.signIn": "Sign In / Register",
|
|
"header.signInAria": "Open sign-in page",
|
|
"header.live": "LIVE",
|
|
"header.refreshAria": "Refresh all data",
|
|
"header.langAria": "Switch language",
|
|
"header.langZh": "中文",
|
|
"header.langEn": "EN",
|
|
|
|
"sidebar.title": "Monitored Cities",
|
|
"sidebar.currentTemp": "Current {temp}",
|
|
"sidebar.peakTempAt": "Peak {temp} @ {time}",
|
|
"sidebar.peakAt": "Peak @ {time}",
|
|
"sidebar.group.high": "Recent Strong",
|
|
"sidebar.group.medium": "Recent Mixed",
|
|
"sidebar.group.low": "Recent Weak",
|
|
"sidebar.group.other": "Low Sample",
|
|
|
|
"dashboard.loading":
|
|
"Synchronizing station observations and settlement station data...",
|
|
|
|
"detail.closeAria": "Close city detail panel",
|
|
"detail.waitSelect": "Waiting for city selection",
|
|
"detail.todayAnalysis": "Today's Intraday",
|
|
"detail.history": "History Reconciliation",
|
|
"detail.loading": "Loading city details...",
|
|
"detail.emptyHint": "Select a city from the left list to view details.",
|
|
"detail.sceneryAlt": "{city} scenery",
|
|
"detail.sceneryTitle": "City Scenery & Microclimate",
|
|
"detail.sceneryFallback":
|
|
"No scenery image matched. You can still review station and observation profile below.",
|
|
"detail.profile": "City Profile",
|
|
"detail.todayMiniTrend": "Today's Intraday Trend (Compact)",
|
|
"detail.chartLegendEmpty":
|
|
"No hourly observations or forecast curve available.",
|
|
|
|
"forecast.title": "Multi-day Forecast",
|
|
"forecast.empty": "No multi-day forecast available",
|
|
"forecast.today": "Today",
|
|
|
|
"guide.title": "📎 PolyWeather Technical Overview",
|
|
"guide.closeAria": "Close technical overview",
|
|
"guide.footer":
|
|
"Primary data sources are METAR, Hong Kong Observatory (HKO), designated NOAA stations, Turkish MGM, Open-Meteo, and weather.gov.",
|
|
|
|
"history.title": "📊 Historical Reconciliation - {city}",
|
|
"history.closeAria": "Close history reconciliation",
|
|
"history.loading": "Loading historical data...",
|
|
"history.error": "Failed to load historical data",
|
|
"history.empty": "No historical records for this city in the last 15 days",
|
|
"history.hitRate": "DEB Settlement Hit Rate (WU)",
|
|
"history.mae": "DEB MAE",
|
|
"history.debHitRate": "DEB Settlement Hit Rate (WU)",
|
|
"history.debMae": "DEB MAE",
|
|
"history.muMae": "μ MAE",
|
|
"history.bestModelMae": "Best Single-model MAE",
|
|
"history.debVsBest": "DEB vs Best Model",
|
|
"history.sample": "Settled Samples (Last 15 Days)",
|
|
"history.sampleDays": "{count} days",
|
|
|
|
"future.todayTitle": "{city} · Intraday Analysis",
|
|
"future.dateTitle": "{city} · {date} Future-date Analysis",
|
|
"future.closeTodayAria": "Close intraday analysis",
|
|
"future.closeDateAria": "Close future-date analysis",
|
|
"future.currentObs": "Current Observation",
|
|
"future.currentWeather": "Current Weather",
|
|
"future.wuRef": "Settlement Ref",
|
|
"future.sunrise": "Sunrise",
|
|
"future.sunset": "Sunset",
|
|
"future.sunshine": "Sunshine Duration",
|
|
"future.todayForecastHigh": "Today's Forecast High",
|
|
"future.targetForecast": "Target-day Forecast",
|
|
"future.deb": "DEB Forecast",
|
|
"future.mu": "Dynamic Distribution Center",
|
|
"future.score": "Trend Score",
|
|
"future.todayTempTrend": "Today's Temperature Trend",
|
|
"future.targetTempTrend": "Target-day Hourly Trend",
|
|
"future.probability": "Model Probability Reference",
|
|
"future.models": "Multi-model Forecast",
|
|
"future.structureToday": "Intraday Structural Signal",
|
|
"future.structureDate": "6-48h Structural Trend",
|
|
"future.judgement": "Judgement",
|
|
"future.confidence": "Confidence",
|
|
"future.maxPrecip": "Max Precip Probability",
|
|
"future.ai": "Airport METAR Narrative",
|
|
"future.noAi":
|
|
"No airport bulletin narrative is available. Structured meteorological and model data are used as baseline.",
|
|
"future.weatherGov": "weather.gov text",
|
|
"future.risk": "Settlement & Deviation Risk",
|
|
"future.climate": "What Mainly Drives Local Climate",
|
|
"future.chartLegendEmpty":
|
|
"No METAR bulletin or hourly observations available",
|
|
|
|
"confidence.high": "High",
|
|
"confidence.medium": "Medium",
|
|
"confidence.low": "Low",
|
|
|
|
"section.todayTempTrend": "Today's Temperature Trend",
|
|
"section.chartEmpty": "No hourly data available",
|
|
"section.probability": "Calibrated Model Probability",
|
|
"section.mu": "Dynamic center μ = {value}{unit}",
|
|
"section.noProb": "No probability data available",
|
|
"section.models": "Multi-model Forecast",
|
|
"section.noModels": "No multi-model forecast available",
|
|
"section.ai": "Airport METAR Narrative",
|
|
"section.aiEmpty":
|
|
"No airport bulletin narrative is available. Structured meteorological data are currently used.",
|
|
"section.risk": "Data Deviation Risk",
|
|
"section.noRiskProfile": "No risk profile available",
|
|
"section.airport": "Airport",
|
|
"section.distance": "Distance",
|
|
"section.note": "Note",
|
|
|
|
"account.title": "Account Center",
|
|
"account.subtitle": "Review identity, access status, and bot binding info",
|
|
"account.backDashboard": "Back to Dashboard",
|
|
"account.refresh": "Refresh",
|
|
"account.signIn": "Sign in / Sign up",
|
|
"account.signOut": "Sign out",
|
|
"account.loading": "Syncing account snapshot...",
|
|
"account.error": "Failed to load: {message}",
|
|
"account.updatedAt": "Last synced: {time}",
|
|
"account.guestName": "PolyWeather User",
|
|
"account.authenticated": "Authenticated",
|
|
"account.guest": "Guest Mode",
|
|
"account.subscriptionActive": "Subscription active",
|
|
"account.subscriptionRequired": "Subscription required",
|
|
"account.subscriptionUnknown": "Subscription unknown",
|
|
"account.card.membership": "Membership & Access",
|
|
"account.card.identity": "Identity",
|
|
"account.card.backend": "Backend Auth",
|
|
"account.card.bot": "Bot Binding",
|
|
"account.field.email": "Email",
|
|
"account.field.userId": "User ID",
|
|
"account.field.provider": "Sign-in method",
|
|
"account.field.lastSignIn": "Last sign-in",
|
|
"account.field.mode": "Entitlement mode",
|
|
"account.field.backendStatus": "Backend status",
|
|
"account.field.subscription": "Subscription result",
|
|
"account.field.requirement": "Subscription policy",
|
|
"account.field.bindCommand": "Binding command",
|
|
"account.field.bindHint":
|
|
"Send this command to the Telegram bot to bind web account identity.",
|
|
"account.mode.supabaseRequired": "Supabase required auth",
|
|
"account.mode.supabaseOptional": "Supabase optional auth",
|
|
"account.mode.supabase": "Supabase session auth",
|
|
"account.mode.legacy": "Legacy token auth",
|
|
"account.mode.disabled": "Auth guard disabled",
|
|
"account.mode.unknown": "Unknown mode",
|
|
"account.backend.ok": "Passed",
|
|
"account.backend.fail": "Failed",
|
|
"account.subscription.active": "Active",
|
|
"account.subscription.inactive": "Inactive/Expired",
|
|
"account.subscription.notRequired": "Not required now",
|
|
"account.subscription.unknown": "Unknown",
|
|
"account.copy": "Copy",
|
|
"account.copied": "Copied",
|
|
"account.na": "--",
|
|
|
|
"common.na": "--",
|
|
},
|
|
};
|
|
|
|
export function normalizeLocale(value?: string | null): Locale {
|
|
if (!value) return DEFAULT_LOCALE;
|
|
const normalized = value.toLowerCase();
|
|
if (normalized.startsWith("en")) return "en-US";
|
|
return "zh-CN";
|
|
}
|
|
|
|
export function getInitialLocaleFromNavigator(): Locale {
|
|
if (typeof window === "undefined") return DEFAULT_LOCALE;
|
|
return normalizeLocale(window.navigator.language);
|
|
}
|
|
|
|
export function formatMessage(
|
|
locale: Locale,
|
|
key: string,
|
|
params?: MessageParams,
|
|
): string {
|
|
const template =
|
|
MESSAGES[locale]?.[key] || MESSAGES[DEFAULT_LOCALE][key] || key;
|
|
if (!params) return template;
|
|
return template.replace(/\{(\w+)\}/g, (_, token) => {
|
|
const value = params[token];
|
|
return value == null ? "" : String(value);
|
|
});
|
|
}
|