401 lines
17 KiB
TypeScript
401 lines
17 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.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": "正在同步站点观测与结算站点信息,请稍候...",
|
|
"dashboard.opportunityTitle": "可交易机会",
|
|
"dashboard.opportunitySubtitle":
|
|
"基于当前时间、实况数据和模型预测,筛选出最具交易价值的市场",
|
|
"dashboard.opportunityList": "机会列表",
|
|
"dashboard.distributionView": "分布视图",
|
|
"dashboard.calendarView": "日历视图",
|
|
|
|
"detail.closeAria": "关闭城市详情面板",
|
|
"detail.waitSelect": "等待选择城市",
|
|
"detail.todayAnalysis": "今日日内分析",
|
|
"detail.loading": "正在加载城市详情...",
|
|
"detail.emptyHint": "从左侧城市列表选择一个城市查看详情。",
|
|
"detail.sceneryAlt": "{city} 风景照",
|
|
"detail.sceneryTitle": "城市风景与微气候",
|
|
"detail.sceneryFallback":
|
|
"当前没有匹配到风景图,可从下方城市档案查看站点与观测结构。",
|
|
"detail.profile": "城市档案",
|
|
"detail.todayMiniTrend": "今日日内走势(简版)",
|
|
"detail.chartLegendEmpty": "暂无小时级实测或预测曲线。",
|
|
"detail.currentTemp": "当前温度",
|
|
"detail.todayHigh": "今日最高",
|
|
"detail.targetTemp": "目标温度",
|
|
"detail.gap": "距目标差距",
|
|
"detail.peakWindow": "预计峰值",
|
|
"detail.compositeScore": "综合得分",
|
|
"detail.selectCity": "← 点击左侧行查看详情",
|
|
"detail.localTime": "当前时间",
|
|
|
|
"forecast.title": "多日预报",
|
|
"forecast.empty": "暂无多日预报",
|
|
"forecast.today": "今天",
|
|
|
|
"guide.title": "📎 PolyWeather 系统技术说明",
|
|
"guide.closeAria": "关闭技术说明",
|
|
"guide.footer":
|
|
"数据源以 METAR、香港天文台(HKO)、NOAA 指定站点、Turkish MGM、Open-Meteo、weather.gov 为主。",
|
|
|
|
|
|
"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": "注意",
|
|
"section.currentConditions": "当前概况",
|
|
"section.tradeRecommendation": "推荐交易",
|
|
|
|
"filter.mode": "扫描模式",
|
|
"filter.tradable": "可交易机会",
|
|
"filter.early": "早期机会",
|
|
"filter.touch": "触达博弈",
|
|
"filter.trend": "趋势确认",
|
|
"filter.conditions": "匹配条件",
|
|
"filter.priceRange": "价格区间",
|
|
"filter.edgeRange": "最小边际优势",
|
|
"filter.onlyTradable": "只允许可交易",
|
|
"filter.liquidity": "流动性级别",
|
|
"filter.liquidityHigh": "最高流动性",
|
|
"filter.timeRange": "时间周期",
|
|
"filter.scanCta": "开始扫描",
|
|
|
|
"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.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...",
|
|
"dashboard.opportunityTitle": "Tradable Opportunities",
|
|
"dashboard.opportunitySubtitle":
|
|
"Identify high-value markets based on real-time data, model predictions, and current time window.",
|
|
"dashboard.opportunityList": "Opportunity List",
|
|
"dashboard.distributionView": "Distribution",
|
|
"dashboard.calendarView": "Calendar",
|
|
|
|
"detail.closeAria": "Close city detail panel",
|
|
"detail.waitSelect": "Waiting for city selection",
|
|
"detail.todayAnalysis": "Today's Intraday",
|
|
"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.",
|
|
"detail.currentTemp": "Current Temp",
|
|
"detail.todayHigh": "Today's High",
|
|
"detail.targetTemp": "Target Temp",
|
|
"detail.gap": "Gap to Target",
|
|
"detail.peakWindow": "Peak Window",
|
|
"detail.compositeScore": "Composite Score",
|
|
"detail.selectCity": "← Select a city from the list",
|
|
"detail.localTime": "Local Time",
|
|
|
|
"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.",
|
|
|
|
|
|
"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",
|
|
"section.currentConditions": "Current Conditions",
|
|
"section.tradeRecommendation": "Recommendation",
|
|
|
|
"filter.mode": "Scan Mode",
|
|
"filter.tradable": "Tradable",
|
|
"filter.early": "Early Stage",
|
|
"filter.touch": "Touch Play",
|
|
"filter.trend": "Trend Confirm",
|
|
"filter.conditions": "Conditions",
|
|
"filter.priceRange": "Price Range",
|
|
"filter.edgeRange": "Min Edge",
|
|
"filter.onlyTradable": "Only Tradable",
|
|
"filter.liquidity": "Liquidity",
|
|
"filter.liquidityHigh": "High Liquidity",
|
|
"filter.timeRange": "Time Period",
|
|
"filter.scanCta": "Start Scan",
|
|
|
|
"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);
|
|
});
|
|
}
|