Skip to main content

futu_core/
diagnostic_text.rs

1//! Stable user-facing diagnostic text keys and shared hint builders.
2//!
3//! Logs should keep stable machine-readable English markers. These helpers are
4//! for CLI / REST / MCP / gateway human hints, where each message also carries
5//! a stable diagnostic code so automation and docs can refer to the same row.
6
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum DiagnosticTextKey {
11    ProtoJsonQotMarketNumeric,
12    QuotePermissionOpenMarket,
13    StaticCacheMissing,
14    OrderbookDetailPermission,
15    JpStockQuotePermission,
16    JpTradePermission,
17    AllDayRiskDisclosure,
18    UpdateCheckUnavailable,
19    Qot3401UnsupportedMarket,
20    Qot3401BackendError,
21    Qot3401EntityIdRequired,
22    Qot3401DiscoveryEmpty,
23    Qot3401SampleUnavailable,
24    QotInvalidCount,
25    QotUnsupportedMarket,
26    QotEventContractRequired,
27    QotMissingParameter,
28    QotInvalidParameter,
29    QotOptionInconsistentMarket,
30    QotOptionDuplicateLeg,
31    QotOptionLegRequired,
32    QotOptionLegLimit,
33    QotUnsupportedSecurityType,
34    QotCapitalUnsupportedSecurityType,
35    QotUsIndexUnsupported,
36    QotInsiderUnsupportedSecurityType,
37}
38
39impl DiagnosticTextKey {
40    pub const fn code(self) -> &'static str {
41        match self {
42            Self::ProtoJsonQotMarketNumeric => "proto_json.qot_market_numeric",
43            Self::QuotePermissionOpenMarket => "quote.permission.open_market",
44            Self::StaticCacheMissing => "static.cache_missing",
45            Self::OrderbookDetailPermission => "quote.permission.orderbook_detail",
46            Self::JpStockQuotePermission => "quote.permission.jp_stock",
47            Self::JpTradePermission => "trade.permission.jp_market",
48            Self::AllDayRiskDisclosure => "trade.risk_disclosure.all_day",
49            Self::UpdateCheckUnavailable => "update.check_unavailable",
50            Self::Qot3401UnsupportedMarket => "qot3401.unsupported_market",
51            Self::Qot3401BackendError => "qot3401.backend_error",
52            Self::Qot3401EntityIdRequired => "qot3401.entity_id_required",
53            Self::Qot3401DiscoveryEmpty => "qot3401.discovery_empty",
54            Self::Qot3401SampleUnavailable => "qot3401.sample_unavailable",
55            Self::QotInvalidCount => "qot.invalid_count",
56            Self::QotUnsupportedMarket => "qot.unsupported_market",
57            Self::QotEventContractRequired => "qot.event_contract_required",
58            Self::QotMissingParameter => "qot.missing_parameter",
59            Self::QotInvalidParameter => "qot.invalid_parameter",
60            Self::QotOptionInconsistentMarket => "qot.option_inconsistent_market",
61            Self::QotOptionDuplicateLeg => "qot.option_duplicate_leg",
62            Self::QotOptionLegRequired => "qot.option_leg_required",
63            Self::QotOptionLegLimit => "qot.option_leg_limit",
64            Self::QotUnsupportedSecurityType => "qot.unsupported_security_type",
65            Self::QotCapitalUnsupportedSecurityType => "qot.capital_unsupported_security_type",
66            Self::QotUsIndexUnsupported => "qot.us_index_unsupported",
67            Self::QotInsiderUnsupportedSecurityType => "qot.insider_unsupported_security_type",
68        }
69    }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum MessageLocale {
74    En,
75    ZhCn,
76    ZhHk,
77}
78
79impl MessageLocale {
80    pub fn from_normalized_lang(lang: &str) -> Self {
81        match lang {
82            "chs" | "zh_CN" | "zh-cn" | "zh-Hans" | "zh_Hans" => Self::ZhCn,
83            "cht" | "zh_HK" | "zh_TW" | "zh-hk" | "zh-tw" | "zh-Hant" | "zh_Hant" => Self::ZhHk,
84            _ => Self::En,
85        }
86    }
87
88    pub fn from_app_lang(app_lang: i32) -> Self {
89        match app_lang {
90            0 => Self::ZhCn,
91            1 => Self::ZhHk,
92            2 => Self::En,
93            _ => Self::En,
94        }
95    }
96}
97
98#[derive(Debug, Clone, Default, PartialEq, Eq)]
99pub struct MessageArgs {
100    values: BTreeMap<&'static str, String>,
101}
102
103impl MessageArgs {
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    pub fn with(mut self, key: &'static str, value: impl Into<String>) -> Self {
109        self.values.insert(key, value.into());
110        self
111    }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct CatalogMessage {
116    pub key: DiagnosticTextKey,
117    pub locale: MessageLocale,
118    pub template: &'static str,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct DiagnosticText {
123    pub key: DiagnosticTextKey,
124    pub text: String,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct DiagnosticTextSpec {
129    key: DiagnosticTextKey,
130    args: MessageArgs,
131}
132
133impl DiagnosticTextSpec {
134    pub fn new(key: DiagnosticTextKey, args: MessageArgs) -> Self {
135        Self { key, args }
136    }
137
138    pub const fn key(&self) -> DiagnosticTextKey {
139        self.key
140    }
141
142    pub fn render_with_app_lang(&self, app_lang: i32) -> DiagnosticText {
143        render_with_app_lang(self.key, app_lang, self.args.clone())
144    }
145}
146
147impl DiagnosticText {
148    pub fn new(key: DiagnosticTextKey, text: impl Into<String>) -> Self {
149        Self {
150            key,
151            text: text.into(),
152        }
153    }
154
155    pub fn render(key: DiagnosticTextKey, locale: MessageLocale, args: MessageArgs) -> Self {
156        let row = catalog_message(key, locale);
157        Self::new(key, render_template(row.template, &args))
158    }
159
160    pub fn text_with_code(&self) -> String {
161        format!("{} [diag:{}]", self.text, self.key.code())
162    }
163}
164
165pub fn render_with_lang(
166    key: DiagnosticTextKey,
167    normalized_lang: &str,
168    args: MessageArgs,
169) -> DiagnosticText {
170    DiagnosticText::render(
171        key,
172        MessageLocale::from_normalized_lang(normalized_lang),
173        args,
174    )
175}
176
177pub fn render_with_app_lang(
178    key: DiagnosticTextKey,
179    app_lang: i32,
180    args: MessageArgs,
181) -> DiagnosticText {
182    DiagnosticText::render(key, MessageLocale::from_app_lang(app_lang), args)
183}
184
185pub fn proto_json_numeric_qot_market_hint() -> DiagnosticText {
186    DiagnosticText::render(
187        DiagnosticTextKey::ProtoJsonQotMarketNumeric,
188        MessageLocale::En,
189        MessageArgs::new(),
190    )
191}
192
193pub fn quote_permission_action_for_market(
194    market_suffix: Option<&str>,
195    symbol: &str,
196) -> DiagnosticText {
197    let market = market_suffix.unwrap_or("market");
198    let market_label = match market_suffix {
199        Some("JP") => "JP stock",
200        Some("US") => "US stock",
201        Some("HK") => "HK stock",
202        Some(market) => market,
203        None => "the market",
204    };
205    DiagnosticText::render(
206        DiagnosticTextKey::QuotePermissionOpenMarket,
207        MessageLocale::En,
208        MessageArgs::new()
209            .with("market", market)
210            .with("market_label", market_label)
211            .with("symbol", symbol),
212    )
213}
214
215pub fn static_cache_missing_message(normalized_symbol: &str, sec_key: &str) -> DiagnosticText {
216    DiagnosticText::render(
217        DiagnosticTextKey::StaticCacheMissing,
218        MessageLocale::En,
219        MessageArgs::new()
220            .with("symbol", normalized_symbol)
221            .with("sec_key", sec_key),
222    )
223}
224
225pub fn orderbook_detail_permission_action() -> DiagnosticText {
226    DiagnosticText::render(
227        DiagnosticTextKey::OrderbookDetailPermission,
228        MessageLocale::En,
229        MessageArgs::new(),
230    )
231}
232
233pub fn jp_stock_quote_permission_hint() -> DiagnosticText {
234    DiagnosticText::render(
235        DiagnosticTextKey::JpStockQuotePermission,
236        MessageLocale::ZhCn,
237        MessageArgs::new(),
238    )
239}
240
241pub fn jp_trade_permission_hint() -> DiagnosticText {
242    DiagnosticText::render(
243        DiagnosticTextKey::JpTradePermission,
244        MessageLocale::ZhCn,
245        MessageArgs::new(),
246    )
247}
248
249pub fn all_day_risk_disclosure_hint(help_url: &str) -> DiagnosticText {
250    DiagnosticText::render(
251        DiagnosticTextKey::AllDayRiskDisclosure,
252        MessageLocale::ZhCn,
253        MessageArgs::new().with("help_url", help_url),
254    )
255}
256
257pub fn update_check_unavailable_action() -> DiagnosticText {
258    DiagnosticText::render(
259        DiagnosticTextKey::UpdateCheckUnavailable,
260        MessageLocale::En,
261        MessageArgs::new(),
262    )
263}
264
265pub fn qot3401_unsupported_market_action() -> DiagnosticText {
266    DiagnosticText::render(
267        DiagnosticTextKey::Qot3401UnsupportedMarket,
268        MessageLocale::En,
269        MessageArgs::new(),
270    )
271}
272
273pub fn qot3401_backend_error_action(api_name: &str, ret_code: i32) -> DiagnosticText {
274    DiagnosticText::render(
275        DiagnosticTextKey::Qot3401BackendError,
276        MessageLocale::En,
277        MessageArgs::new()
278            .with("api_name", api_name)
279            .with("ret_code", ret_code.to_string()),
280    )
281}
282
283pub fn qot3401_entity_id_required_action() -> DiagnosticText {
284    DiagnosticText::render(
285        DiagnosticTextKey::Qot3401EntityIdRequired,
286        MessageLocale::En,
287        MessageArgs::new(),
288    )
289}
290
291pub fn qot3401_discovery_empty_action() -> DiagnosticText {
292    DiagnosticText::render(
293        DiagnosticTextKey::Qot3401DiscoveryEmpty,
294        MessageLocale::En,
295        MessageArgs::new(),
296    )
297}
298
299pub fn qot3401_sample_unavailable_action() -> DiagnosticText {
300    DiagnosticText::render(
301        DiagnosticTextKey::Qot3401SampleUnavailable,
302        MessageLocale::En,
303        MessageArgs::new(),
304    )
305}
306
307fn catalog_message(key: DiagnosticTextKey, locale: MessageLocale) -> CatalogMessage {
308    let template = match (key, locale) {
309        (DiagnosticTextKey::ProtoJsonQotMarketNumeric, MessageLocale::ZhCn) => {
310            "`market` 是生成的 proto 数字枚举,不是字符串。请使用 Qot_Common.QotMarket 数值,例如 HK=1, US=11, SH=21, SZ=22, SG=31, JP=41, AU=51, MY=61。示例:{\"market\":11}"
311        }
312        (DiagnosticTextKey::ProtoJsonQotMarketNumeric, MessageLocale::ZhHk) => {
313            "`market` 是生成的 proto 數字枚舉,不是字串。請使用 Qot_Common.QotMarket 數值,例如 HK=1, US=11, SH=21, SZ=22, SG=31, JP=41, AU=51, MY=61。示例:{\"market\":11}"
314        }
315        (DiagnosticTextKey::ProtoJsonQotMarketNumeric, MessageLocale::En) => {
316            "`market` is a numeric generated proto enum, not a string. Use Qot_Common.QotMarket values such as HK=1, US=11, SH=21, SZ=22, SG=31, JP=41, AU=51, MY=61. Example: {\"market\":11}"
317        }
318
319        (DiagnosticTextKey::QuotePermissionOpenMarket, MessageLocale::ZhCn) => {
320            "先运行 futucli quote-rights;请在官方 Futu/moomoo App 开通{market_label}行情权限,然后用 futucli quote-rights 和 futucli quote-capability {symbol} 复查。"
321        }
322        (DiagnosticTextKey::QuotePermissionOpenMarket, MessageLocale::ZhHk) => {
323            "先執行 futucli quote-rights;請在官方 Futu/moomoo App 開通{market_label}行情權限,然後用 futucli quote-rights 和 futucli quote-capability {symbol} 複查。"
324        }
325        (DiagnosticTextKey::QuotePermissionOpenMarket, MessageLocale::En) => {
326            "run futucli quote-rights; open {market_label} quote permission in the official Futu/moomoo App if it remains zero, then recheck with futucli quote-rights and futucli quote-capability {symbol}"
327        }
328
329        (DiagnosticTextKey::StaticCacheMissing, MessageLocale::ZhCn) => {
330            "静态缓存缺少 {symbol} ({sec_key});请等待 stock-list sync 收敛,或先调用 static/static-warmup。"
331        }
332        (DiagnosticTextKey::StaticCacheMissing, MessageLocale::ZhHk) => {
333            "靜態快取缺少 {symbol} ({sec_key});請等待 stock-list sync 收斂,或先呼叫 static/static-warmup。"
334        }
335        (DiagnosticTextKey::StaticCacheMissing, MessageLocale::En) => {
336            "static cache missing {symbol} ({sec_key}); wait stock-list sync or call static/static-warmup first"
337        }
338
339        (DiagnosticTextKey::OrderbookDetailPermission, MessageLocale::ZhCn) => {
340            "请检查 quote-rights,并确认订阅账户具备所需 orderbook detail/depth 行情权限。"
341        }
342        (DiagnosticTextKey::OrderbookDetailPermission, MessageLocale::ZhHk) => {
343            "請檢查 quote-rights,並確認訂閱帳戶具備所需 orderbook detail/depth 行情權限。"
344        }
345        (DiagnosticTextKey::OrderbookDetailPermission, MessageLocale::En) => {
346            "check quote-rights and subscribe with the required orderbook detail permission"
347        }
348
349        (DiagnosticTextKey::JpStockQuotePermission, MessageLocale::ZhCn) => {
350            "App 延时行情不等于 OpenD API 实时行情权限;请在官方 Futu/moomoo App 开通日股实时行情 / Premium 权限,然后用 futucli quote-rights 确认 JP stock 权限。"
351        }
352        (DiagnosticTextKey::JpStockQuotePermission, MessageLocale::ZhHk) => {
353            "App 延時行情不等於 OpenD API 即時行情權限;請在官方 Futu/moomoo App 開通日股即時行情 / Premium 權限,然後用 futucli quote-rights 確認 JP stock 權限。"
354        }
355        (DiagnosticTextKey::JpStockQuotePermission, MessageLocale::En) => {
356            "App delayed JP quotes do not imply OpenD API realtime quote permission; open JP realtime / Premium quote permission in the official Futu/moomoo App, then confirm JP stock permission with futucli quote-rights."
357        }
358
359        (DiagnosticTextKey::JpTradePermission, MessageLocale::ZhCn) => {
360            "当前账户未开通日股交易权限(JP / trd_market=15)。请在 Futu/moomoo App 中开通日股交易权限,并按提示完成相关协议、风险披露、税务/市场权限流程。开通后重新查询账户,确认 trdmarket_auth 包含 JP/15,再重试下单。App 路径建议:账户 > 更多/交易权限 > 日股交易权限(以当前 App 实际入口为准)"
361        }
362        (DiagnosticTextKey::JpTradePermission, MessageLocale::ZhHk) => {
363            "當前帳戶未開通日股交易權限(JP / trd_market=15)。請在 Futu/moomoo App 中開通日股交易權限,並按提示完成相關協議、風險披露、稅務/市場權限流程。開通後重新查詢帳戶,確認 trdmarket_auth 包含 JP/15,再重試下單。App 路徑建議:帳戶 > 更多/交易權限 > 日股交易權限(以當前 App 實際入口為準)"
364        }
365        (DiagnosticTextKey::JpTradePermission, MessageLocale::En) => {
366            "The account does not have JP trading permission (JP / trd_market=15). Open JP trading permission in the Futu/moomoo App, finish the required agreements, risk disclosure, tax and market permission steps, then query accounts again and confirm trdmarket_auth contains JP/15 before retrying the order."
367        }
368
369        (DiagnosticTextKey::AllDayRiskDisclosure, MessageLocale::ZhCn) => {
370            "后端要求先完成全时段交易风险披露。请在官方 Futu/moomoo App 内完成全时段交易相关风险披露后重试;daemon 只透传下单协议,无法代用户确认该披露。官方帮助入口:{help_url}"
371        }
372        (DiagnosticTextKey::AllDayRiskDisclosure, MessageLocale::ZhHk) => {
373            "後端要求先完成全時段交易風險披露。請在官方 Futu/moomoo App 內完成全時段交易相關風險披露後重試;daemon 只透傳下單協議,無法代用戶確認該披露。官方幫助入口:{help_url}"
374        }
375        (DiagnosticTextKey::AllDayRiskDisclosure, MessageLocale::En) => {
376            "The backend requires all-day trading risk disclosure before this order. Complete the all-day trading risk disclosure in the official Futu/moomoo App, then retry. The daemon only forwards order protocols and cannot confirm this disclosure for the user. Help: {help_url}"
377        }
378
379        (DiagnosticTextKey::UpdateCheckUnavailable, MessageLocale::ZhCn) => {
380            "更新检查不可用;稍后重跑 `futucli version --check`,或用 `--url` / FUTU_UPDATE_CHECK_URL 指定 manifest,也可以用 `futucli doctor --no-update-check` 跳过 doctor 的更新检查。"
381        }
382        (DiagnosticTextKey::UpdateCheckUnavailable, MessageLocale::ZhHk) => {
383            "更新檢查不可用;稍後重跑 `futucli version --check`,或用 `--url` / FUTU_UPDATE_CHECK_URL 指定 manifest,也可以用 `futucli doctor --no-update-check` 跳過 doctor 的更新檢查。"
384        }
385        (DiagnosticTextKey::UpdateCheckUnavailable, MessageLocale::En) => {
386            "update check unavailable; rerun `futucli version --check` later, override with `--url` / FUTU_UPDATE_CHECK_URL, or skip doctor with `futucli doctor --no-update-check`"
387        }
388
389        (DiagnosticTextKey::Qot3401UnsupportedMarket, MessageLocale::ZhCn) => {
390            "该 3401+ 接口当前不支持传入的 market。请使用数字 Qot_Common.QotMarket,并先用包内 examples/qot-3401-plus-proto-json-examples.json 查看该接口已登记的市场样例。常用值:HK=1, US=11, SH=21, SZ=22, SG=31, JP=41, AU=51, MY=61, CA=71。"
391        }
392        (DiagnosticTextKey::Qot3401UnsupportedMarket, MessageLocale::ZhHk) => {
393            "該 3401+ 介面目前不支援傳入的 market。請使用數字 Qot_Common.QotMarket,並先用包內 examples/qot-3401-plus-proto-json-examples.json 查看該介面已登記的市場示例。常用值:HK=1, US=11, SH=21, SZ=22, SG=31, JP=41, AU=51, MY=61, CA=71。"
394        }
395        (DiagnosticTextKey::Qot3401UnsupportedMarket, MessageLocale::En) => {
396            "this 3401+ endpoint does not support the supplied market; use numeric Qot_Common.QotMarket values and check examples/qot-3401-plus-proto-json-examples.json for the endpoint sample. Common values: HK=1, US=11, SH=21, SZ=22, SG=31, JP=41, AU=51, MY=61, CA=71"
397        }
398
399        (DiagnosticTextKey::Qot3401BackendError, MessageLocale::ZhCn) => {
400            "{api_name} 后端返回 ret={ret_code}。请先运行对应发现入口并使用返回 ID;如果发现入口也返回空或 ret=-1,请把 market、C2S JSON、daemon 日志和 qot_3401_plus_discovery_chain_smoke.py 输出一起回传。"
401        }
402        (DiagnosticTextKey::Qot3401BackendError, MessageLocale::ZhHk) => {
403            "{api_name} 後端返回 ret={ret_code}。請先執行對應發現入口並使用返回 ID;如果發現入口也返回空或 ret=-1,請把 market、C2S JSON、daemon 日誌和 qot_3401_plus_discovery_chain_smoke.py 輸出一起回傳。"
404        }
405        (DiagnosticTextKey::Qot3401BackendError, MessageLocale::En) => {
406            "{api_name} backend returned ret={ret_code}; run the discovery list endpoint first and use the returned id. If discovery is empty or also returns ret=-1, attach market, C2S JSON, daemon logs, and qot_3401_plus_discovery_chain_smoke.py output to the bug report"
407        }
408
409        (DiagnosticTextKey::Qot3401EntityIdRequired, MessageLocale::ZhCn) => {
410            "该详情接口需要有效 ID。机构详情请先跑 institution-list 取 institution_id;产业链详情请先跑 industrial-chain-list 取 chain_id;板块详情请从 heat-map-data / plate-list / chain-detail 返回的 plate_id 继续查询。"
411        }
412        (DiagnosticTextKey::Qot3401EntityIdRequired, MessageLocale::ZhHk) => {
413            "該詳情介面需要有效 ID。機構詳情請先跑 institution-list 取 institution_id;產業鏈詳情請先跑 industrial-chain-list 取 chain_id;板塊詳情請從 heat-map-data / plate-list / chain-detail 返回的 plate_id 繼續查詢。"
414        }
415        (DiagnosticTextKey::Qot3401EntityIdRequired, MessageLocale::En) => {
416            "this detail endpoint needs a valid id. Run institution-list for institution_id, industrial-chain-list for chain_id, and heat-map-data / plate-list / chain-detail for plate_id before querying detail endpoints"
417        }
418
419        (DiagnosticTextKey::Qot3401DiscoveryEmpty, MessageLocale::ZhCn) => {
420            "发现入口没有返回可用 ID。请换 HK/US 等已登记市场样例重试;若仍为空,把请求 JSON 和 daemon 日志回传,先不要把详情接口当作可用路径。"
421        }
422        (DiagnosticTextKey::Qot3401DiscoveryEmpty, MessageLocale::ZhHk) => {
423            "發現入口沒有返回可用 ID。請換 HK/US 等已登記市場示例重試;若仍為空,把請求 JSON 和 daemon 日誌回傳,先不要把詳情介面當作可用路徑。"
424        }
425        (DiagnosticTextKey::Qot3401DiscoveryEmpty, MessageLocale::En) => {
426            "the discovery endpoint returned no usable id; retry with a registered HK/US sample and attach request JSON plus daemon logs before treating detail endpoints as usable"
427        }
428
429        (DiagnosticTextKey::Qot3401SampleUnavailable, MessageLocale::ZhCn) => {
430            "当前请求缺少已验证生产样例。请参考包内 examples/qot-3401-plus-proto-json-examples.json,用真实后端成功样例补齐后再对外宣称可用。"
431        }
432        (DiagnosticTextKey::Qot3401SampleUnavailable, MessageLocale::ZhHk) => {
433            "當前請求缺少已驗證生產示例。請參考包內 examples/qot-3401-plus-proto-json-examples.json,用真實後端成功示例補齊後再對外宣稱可用。"
434        }
435        (DiagnosticTextKey::Qot3401SampleUnavailable, MessageLocale::En) => {
436            "this request has no verified production sample yet; use examples/qot-3401-plus-proto-json-examples.json and add a real backend success sample before claiming the path is usable"
437        }
438
439        (DiagnosticTextKey::QotInvalidCount, MessageLocale::ZhCn) => {
440            "{endpoint} 请求数量必须在 {min} 到 {max} 之间。"
441        }
442        (DiagnosticTextKey::QotInvalidCount, MessageLocale::ZhHk) => {
443            "{endpoint} 請求數量必須在 {min} 到 {max} 之間。"
444        }
445        (DiagnosticTextKey::QotInvalidCount, MessageLocale::En) => {
446            "{endpoint} request count must be between {min} and {max}"
447        }
448
449        (DiagnosticTextKey::QotUnsupportedMarket, MessageLocale::ZhCn) => {
450            "{endpoint} 当前市场 {market} 不支持该接口。"
451        }
452        (DiagnosticTextKey::QotUnsupportedMarket, MessageLocale::ZhHk) => {
453            "{endpoint} 當前市場 {market} 不支援該介面。"
454        }
455        (DiagnosticTextKey::QotUnsupportedMarket, MessageLocale::En) => {
456            "{endpoint} market {market} is not supported"
457        }
458
459        (DiagnosticTextKey::QotEventContractRequired, MessageLocale::ZhCn) => {
460            "{endpoint} 仅支持事件合约,且需要相应行情权限:{market}.{code}。"
461        }
462        (DiagnosticTextKey::QotEventContractRequired, MessageLocale::ZhHk) => {
463            "{endpoint} 僅支援事件合約,且需要相應行情權限:{market}.{code}。"
464        }
465        (DiagnosticTextKey::QotEventContractRequired, MessageLocale::En) => {
466            "{endpoint} requires an event contract with quote permission: {market}.{code}"
467        }
468
469        (DiagnosticTextKey::QotMissingParameter, MessageLocale::ZhCn) => {
470            "{endpoint} 缺少必要参数 {param}。"
471        }
472        (DiagnosticTextKey::QotMissingParameter, MessageLocale::ZhHk) => {
473            "{endpoint} 缺少必要參數 {param}。"
474        }
475        (DiagnosticTextKey::QotMissingParameter, MessageLocale::En) => {
476            "{endpoint} missing required parameter {param}"
477        }
478
479        (DiagnosticTextKey::QotInvalidParameter, MessageLocale::ZhCn) => {
480            "{endpoint} 参数 {param} 的值 {value} 无效(索引 {index})。"
481        }
482        (DiagnosticTextKey::QotInvalidParameter, MessageLocale::ZhHk) => {
483            "{endpoint} 參數 {param} 的值 {value} 無效(索引 {index})。"
484        }
485        (DiagnosticTextKey::QotInvalidParameter, MessageLocale::En) => {
486            "{endpoint} parameter {param} has invalid value {value} at index {index}"
487        }
488
489        (DiagnosticTextKey::QotOptionInconsistentMarket, MessageLocale::ZhCn) => {
490            "{endpoint} 组合腿市场必须一致。"
491        }
492        (DiagnosticTextKey::QotOptionInconsistentMarket, MessageLocale::ZhHk) => {
493            "{endpoint} 組合腿市場必須一致。"
494        }
495        (DiagnosticTextKey::QotOptionInconsistentMarket, MessageLocale::En) => {
496            "{endpoint} combo leg markets must match"
497        }
498
499        (DiagnosticTextKey::QotOptionDuplicateLeg, MessageLocale::ZhCn) => {
500            "{endpoint} 组合腿不能重复。"
501        }
502        (DiagnosticTextKey::QotOptionDuplicateLeg, MessageLocale::ZhHk) => {
503            "{endpoint} 組合腿不能重複。"
504        }
505        (DiagnosticTextKey::QotOptionDuplicateLeg, MessageLocale::En) => {
506            "{endpoint} combo legs must not contain duplicates"
507        }
508
509        (DiagnosticTextKey::QotOptionLegRequired, MessageLocale::ZhCn) => {
510            "{endpoint} 至少需要一个期权腿。"
511        }
512        (DiagnosticTextKey::QotOptionLegRequired, MessageLocale::ZhHk) => {
513            "{endpoint} 至少需要一個期權腿。"
514        }
515        (DiagnosticTextKey::QotOptionLegRequired, MessageLocale::En) => {
516            "{endpoint} at least one option leg is required"
517        }
518
519        (DiagnosticTextKey::QotOptionLegLimit, MessageLocale::ZhCn) => {
520            "{endpoint} 期权腿数量不能超过 {max}。"
521        }
522        (DiagnosticTextKey::QotOptionLegLimit, MessageLocale::ZhHk) => {
523            "{endpoint} 期權腿數量不能超過 {max}。"
524        }
525        (DiagnosticTextKey::QotOptionLegLimit, MessageLocale::En) => {
526            "{endpoint} option leg count must not exceed {max}"
527        }
528
529        (DiagnosticTextKey::QotUnsupportedSecurityType, MessageLocale::ZhCn) => {
530            "{endpoint} 不支持第 {index} 个组合腿的证券类型 {secType}。"
531        }
532        (DiagnosticTextKey::QotUnsupportedSecurityType, MessageLocale::ZhHk) => {
533            "{endpoint} 不支援第 {index} 個組合腿的證券類型 {secType}。"
534        }
535        (DiagnosticTextKey::QotUnsupportedSecurityType, MessageLocale::En) => {
536            "{endpoint} security type {secType} for combo leg {index} is not supported"
537        }
538
539        (DiagnosticTextKey::QotCapitalUnsupportedSecurityType, MessageLocale::ZhCn) => {
540            "{endpoint} 仅支持正股、信托、窝轮和数字货币。"
541        }
542        (DiagnosticTextKey::QotCapitalUnsupportedSecurityType, MessageLocale::ZhHk) => {
543            "{endpoint} 僅支援正股、信託、窩輪和數字貨幣。"
544        }
545        (DiagnosticTextKey::QotCapitalUnsupportedSecurityType, MessageLocale::En) => {
546            "{endpoint} supports only equity, trust, warrant, and crypto securities"
547        }
548
549        (DiagnosticTextKey::QotUsIndexUnsupported, MessageLocale::ZhCn) => {
550            "{endpoint} 暂不支持美股指数 {code}。"
551        }
552        (DiagnosticTextKey::QotUsIndexUnsupported, MessageLocale::ZhHk) => {
553            "{endpoint} 暫不支援美股指數 {code}。"
554        }
555        (DiagnosticTextKey::QotUsIndexUnsupported, MessageLocale::En) => {
556            "{endpoint} does not support US index {code}"
557        }
558
559        (DiagnosticTextKey::QotInsiderUnsupportedSecurityType, MessageLocale::ZhCn) => {
560            "{endpoint} 仅支持美股或新加坡市场的正股和信托;证券类型 {secType} 不受支持。"
561        }
562        (DiagnosticTextKey::QotInsiderUnsupportedSecurityType, MessageLocale::ZhHk) => {
563            "{endpoint} 僅支援美股或新加坡市場的正股和信託;證券類型 {secType} 不受支援。"
564        }
565        (DiagnosticTextKey::QotInsiderUnsupportedSecurityType, MessageLocale::En) => {
566            "{endpoint} supports only US or SG equities and trusts; security type {secType} is unsupported"
567        }
568    };
569    CatalogMessage {
570        key,
571        locale,
572        template,
573    }
574}
575
576fn render_template(template: &str, args: &MessageArgs) -> String {
577    let mut rendered = template.to_string();
578    for (key, value) in &args.values {
579        rendered = rendered.replace(&format!("{{{key}}}"), value);
580        rendered = rendered.replace(&format!("{{{{{key}}}}}"), value);
581    }
582    rendered
583}