Skip to content

Commit eb3cdd8

Browse files
Add interactive language switch and tests
Introduce an interactive UI language switch: add i18n strings for language menu/prompts, implement run_langint and set_interact_lang to apply and persist default_lang, and surface the new 'l' menu option. Improve init_bsp/pick_bsp handling to accept quit input, offer optional built-in series ephemeris fallback, and centralize applying language from config after changes. Update CMakeLists to include a new test and add tests/test_interact.cpp covering BSP download prompt and language-switch persistence. Misc: minor I/O/error handling tweaks and config reload after updates.
1 parent 46e2037 commit eb3cdd8

File tree

5 files changed

+257
-16
lines changed

5 files changed

+257
-16
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ lunar completion bash|zsh|fish|powershell
581581
- `14` config
582582
- `15` completion
583583
- `d` 切换/下载 BSP
584+
- `l` 切换语言
584585
- `h` 帮助
585586
- `q` 退出
586587
@@ -590,6 +591,7 @@ lunar completion bash|zsh|fish|powershell
590591
591592
- 命令行:`--lang zh|zht|en|ja|ko`
592593
- 配置默认:`default_lang`
594+
- 交互模式:主菜单 `l` 可即时切换并写回 `default_lang`
593595
594596
优先级:命令行高于配置默认。
595597

src/i18n/catalog_interact.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,38 @@ const Item kItems[]={{"done_back",
211211
"Completion shell: 1) bash 2) zsh 3) fish 4) powershell (default powershell): ",
212212
"補完シェル: 1) bash 2) zsh 3) fish 4) powershell(既定 powershell): ",
213213
"완성 셸: 1) bash 2) zsh 3) fish 4) powershell (기본 powershell): "},
214+
{"menu.lang","切换界面语言","Change language","表示言語を切り替え","언어 변경"},
215+
{"lang.current","当前界面语言:{0}","Current UI language: {0}","現在の表示言語: {0}","현재 UI 언어: {0}"},
216+
{"prompt.lang_choice",
217+
"选择语言:1) 简体中文 2) 繁體中文 3) English 4) 日本語 5) 한국어(也可直接输入 zh|zht|en|ja|ko,回车返回):",
218+
"Choose language: 1) Simplified Chinese 2) Traditional Chinese 3) English 4) Japanese 5) Korean (or enter zh|zht|en|ja|ko, Enter to go back): ",
219+
"言語を選択: 1) 簡体中文 2) 繁體中文 3) English 4) 日本語 5) 한국어(zh|zht|en|ja|ko を直接入力可、Enter で戻る): ",
220+
"언어 선택: 1) 간체 중국어 2) 번체 중국어 3) English 4) 日本語 5) 한국어 (zh|zht|en|ja|ko 직접 입력 가능, Enter로 돌아감): "},
221+
{"lang.updated",
222+
"界面语言已切换为 {0},并已写入 default_lang。",
223+
"UI language switched to {0} and saved to default_lang.",
224+
"表示言語を {0} に切り替え、default_lang に保存しました。",
225+
"UI 언어를 {0}(으)로 변경했고 default_lang에 저장했습니다."},
226+
{"lang.invalid",
227+
"无效的语言选项,请输入 1-5 或 zh|zht|en|ja|ko。",
228+
"Invalid language choice. Enter 1-5 or zh|zht|en|ja|ko.",
229+
"無効な言語選択です。1-5 または zh|zht|en|ja|ko を入力してください。",
230+
"잘못된 언어 선택입니다. 1-5 또는 zh|zht|en|ja|ko 를 입력하세요."},
231+
{"info.no_bsp_found",
232+
"未找到可用 BSP 文件,下面可直接下载。",
233+
"No usable BSP files found. You can download one below.",
234+
"利用可能な BSP ファイルが見つかりません。以下からダウンロードできます。",
235+
"사용 가능한 BSP 파일을 찾지 못했습니다. 아래에서 바로 다운로드할 수 있습니다."},
236+
{"prompt.use_series_fallback",
237+
"未下载 BSP。是否改用内置 VSOP87A/ELPMPP02 星历?",
238+
"No BSP downloaded. Use the built-in VSOP87A/ELPMPP02 ephemeris instead?",
239+
"BSP をダウンロードしていません。内蔵 VSOP87A/ELPMPP02 星暦を使用しますか?",
240+
"BSP를 다운로드하지 않았습니다. 내장 VSOP87A/ELPMPP02 천체력을 대신 사용할까요?"},
241+
{"info.series_selected",
242+
"已切换到内置 VSOP87A/ELPMPP02 星历。",
243+
"Switched to the built-in VSOP87A/ELPMPP02 ephemeris.",
244+
"内蔵 VSOP87A/ELPMPP02 星暦へ切り替えました。",
245+
"내장 VSOP87A/ELPMPP02 천체력으로 전환했습니다."},
214246
{"err.empty_required",
215247
"{0}不能为空",
216248
"{0} cannot be empty",

src/interact.cpp

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,58 @@ std::string itx(const std::string&key,const std::string&a0){
237237
return lunar::i18n::interact::textf(key,a0);
238238
}
239239

240+
void apply_lang_from_cfg(const InterCfg&cfg){
241+
lunar::i18n::Lang lang=lunar::i18n::Lang::Zh;
242+
if(!cfg.default_lang.empty()&&lunar::i18n::try_parse_lang(cfg.default_lang,&lang)){
243+
lunar::i18n::set_lang(lang);
244+
}
245+
}
246+
247+
bool set_interact_lang(InterCfg&cfg,const std::string&choice){
248+
lunar::i18n::Lang lang=lunar::i18n::Lang::Zh;
249+
if(choice=="1"){
250+
lang=lunar::i18n::Lang::Zh;
251+
}else if(choice=="2"){
252+
lang=lunar::i18n::Lang::ZhHant;
253+
}else if(choice=="3"){
254+
lang=lunar::i18n::Lang::En;
255+
}else if(choice=="4"){
256+
lang=lunar::i18n::Lang::Ja;
257+
}else if(choice=="5"){
258+
lang=lunar::i18n::Lang::Ko;
259+
}else if(!lunar::i18n::try_parse_lang(choice,&lang)){
260+
return false;
261+
}
262+
cfg.default_lang=lunar::i18n::lang_code(lang);
263+
lunar::i18n::set_lang(lang);
264+
save_cfg(cfg);
265+
return true;
266+
}
267+
268+
std::string use_series_ephem(InterCfg&cfg){
269+
#if LUNAR_ENABLE_SERIES_FALLBACK
270+
cfg.def_bsp=kSeriesEphemToken;
271+
add_bsp_if_missing(cfg,cfg.def_bsp);
272+
save_cfg(cfg);
273+
std::cout<<itx("info.series_selected")<<std::endl;
274+
return cfg.def_bsp;
275+
#else
276+
(void)cfg;
277+
return "";
278+
#endif
279+
}
280+
281+
std::string maybe_use_series_ephem(InterCfg&cfg){
282+
#if LUNAR_ENABLE_SERIES_FALLBACK
283+
if(ask_yes_no(itx("prompt.use_series_fallback"),false)){
284+
return use_series_ephem(cfg);
285+
}
286+
#else
287+
(void)cfg;
288+
#endif
289+
return "";
290+
}
291+
240292
std::string pick_bsp(InterCfg&cfg){
241293
auto options=bsp_opts();
242294
std::cout<<"\n"<<lunar::i18n::pick("可下载的 BSP 星历:",
@@ -377,13 +429,17 @@ std::string init_bsp(InterCfg&cfg){
377429
"사용할 BSP 번호를 선택하세요 (0은 다운로드 화면): ");
378430
std::string sel;
379431
std::getline(std::cin,sel);
380-
if(!sel.empty()&&std::isdigit(static_cast<unsigned char>(sel[0]))){
432+
if(sel.empty()||sel=="q"||sel=="Q"){
433+
return "";
434+
}
435+
if(std::isdigit(static_cast<unsigned char>(sel[0]))){
381436
int idx=std::stoi(sel);
382437
if(idx==0){
383438
std::string downloaded=pick_bsp(cfg);
384439
if(!downloaded.empty()){
385440
return downloaded;
386441
}
442+
return maybe_use_series_ephem(cfg);
387443
}else if(idx>=1&&static_cast<std::size_t>(idx)<=bsp_files.size()){
388444
cfg.def_bsp=bsp_files[idx-1].string();
389445
add_bsp_if_missing(cfg,cfg.def_bsp);
@@ -394,26 +450,20 @@ std::string init_bsp(InterCfg&cfg){
394450
return cfg.def_bsp;
395451
}
396452
}
453+
std::cout<<itx("invalid_option")<<std::endl;
454+
return "";
397455
}
398456

399457
std::cout<<lunar::i18n::pick("未找到 BSP 文件,将进入下载界面。",
400458
"No BSP found. Opening downloader.",
401459
"BSP が見つからないためダウンロード画面を開きます。",
402460
"BSP 파일을 찾지 못해 다운로드 화면으로 이동합니다.")
403461
<<std::endl;
404-
#if LUNAR_ENABLE_SERIES_FALLBACK
405-
cfg.def_bsp=kSeriesEphemToken;
406-
add_bsp_if_missing(cfg,cfg.def_bsp);
407-
save_cfg(cfg);
408-
std::cout<<lunar::i18n::pick("将自动切换到内置 VSOP87A/ELPMPP02 星历。",
409-
"Switching to built-in VSOP87A/ELPMPP02 ephemeris.",
410-
"内蔵 VSOP87A/ELPMPP02 星暦へ切り替えます。",
411-
"?? VSOP87A/ELPMPP02 ??? ?? ????.")
412-
<<std::endl;
413-
return cfg.def_bsp;
414-
#else
415-
return pick_bsp(cfg);
416-
#endif
462+
std::string ephem=pick_bsp(cfg);
463+
if(!ephem.empty()){
464+
return ephem;
465+
}
466+
return maybe_use_series_ephem(cfg);
417467
}
418468

419469
void int_month(const std::string&ephem){
@@ -859,7 +909,27 @@ void run_aint(const std::string&ephem){
859909
ask_line(done_back_msg());
860910
}
861911

862-
void run_cfgint(){
912+
void run_langint(InterCfg&cfg){
913+
std::cout<<itx("lang.current",lunar::i18n::current_lang_code())<<std::endl;
914+
std::cout<<"[1] 简体中文 (zh)\n";
915+
std::cout<<"[2] 繁體中文 (zht)\n";
916+
std::cout<<"[3] English (en)\n";
917+
std::cout<<"[4] 日本語 (ja)\n";
918+
std::cout<<"[5] 한국어 (ko)\n";
919+
std::string choice=ask_line(itx("prompt.lang_choice"));
920+
if(choice.empty()||choice=="q"||choice=="Q"){
921+
return;
922+
}
923+
if(!set_interact_lang(cfg,choice)){
924+
std::cout<<itx("lang.invalid")<<std::endl;
925+
ask_line(back_msg());
926+
return;
927+
}
928+
std::cout<<itx("lang.updated",cfg.default_lang)<<std::endl;
929+
ask_line(done_back_msg());
930+
}
931+
932+
void run_cfgint(InterCfg&cfg){
863933
std::string act=ask_line(itx("prompt.config_action"));
864934
if(act=="2"||act=="set"){
865935
std::string key=ask_line(itx("prompt.config_key"));
@@ -872,6 +942,8 @@ void run_cfgint(){
872942
}
873943
std::vector<std::string> args={"set",key,value};
874944
cmd_cfg(args);
945+
load_cfg(cfg);
946+
apply_lang_from_cfg(cfg);
875947
ask_line(done_back_msg());
876948
return;
877949
}
@@ -952,6 +1024,7 @@ void int_mode(){
9521024
std::cout<<"[14] "<<itx("menu.config")<<" (config)\n";
9531025
std::cout<<"[15] "<<itx("menu.completion")<<" (completion)\n";
9541026
std::cout<<"[d] "<<itx("menu.switch_bsp")<<"\n";
1027+
std::cout<<"[l] "<<itx("menu.lang")<<"\n";
9551028
std::cout<<"[h] "<<itx("menu.help")<<"\n";
9561029
std::cout<<"[q] "<<itx("menu.exit")<<"\n";
9571030
std::string choice=ask_line(itx("input_select"));
@@ -983,14 +1056,16 @@ void int_mode(){
9831056
}else if(choice=="13"){
9841057
run_with_err("almanac",[&](){ run_aint(ephem); });
9851058
}else if(choice=="14"){
986-
run_with_err("config",[&](){ run_cfgint(); });
1059+
run_with_err("config",[&](){ run_cfgint(cfg); });
9871060
}else if(choice=="15"){
9881061
run_with_err("completion",[&](){ run_pint(); });
9891062
}else if(choice=="d"||choice=="D"){
9901063
std::string new_ephem=init_bspq(cfg);
9911064
if(!new_ephem.empty()){
9921065
ephem=new_ephem;
9931066
}
1067+
}else if(choice=="l"||choice=="L"){
1068+
run_with_err("lang",[&](){ run_langint(cfg); });
9941069
}else if(choice=="h"||choice=="H"){
9951070
use_main();
9961071
ask_line(back_msg());

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ add_executable(lunar_tests
2121
test_core_cli.cpp
2222
test_eclipse.cpp
2323
test_almanac_i18n.cpp
24+
test_interact.cpp
2425
)
2526
lunar_enable_runtime_optimizations(lunar_tests)
2627

tests/test_interact.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#include "test_common.hpp"
2+
3+
#include<filesystem>
4+
#include<fstream>
5+
#include<iostream>
6+
#include<sstream>
7+
#include<stdexcept>
8+
#include<string>
9+
10+
#include<gtest/gtest.h>
11+
12+
#include "lunar/i18n.hpp"
13+
#include "lunar/interact.hpp"
14+
15+
namespace{
16+
17+
class ScopedCin{
18+
public:
19+
explicit ScopedCin(const std::string&text):
20+
input_(text),old_(std::cin.rdbuf(input_.rdbuf())){}
21+
22+
~ScopedCin(){ std::cin.rdbuf(old_); }
23+
24+
private:
25+
std::istringstream input_;
26+
std::streambuf*old_;
27+
};
28+
29+
class ScopedCout{
30+
public:
31+
ScopedCout():old_(std::cout.rdbuf(output_.rdbuf())){}
32+
33+
~ScopedCout(){ std::cout.rdbuf(old_); }
34+
35+
std::string str()const{ return output_.str(); }
36+
37+
private:
38+
std::ostringstream output_;
39+
std::streambuf*old_;
40+
};
41+
42+
class ScopedCwd{
43+
public:
44+
explicit ScopedCwd(const std::filesystem::path&path):
45+
old_(std::filesystem::current_path()){
46+
std::filesystem::current_path(path);
47+
}
48+
49+
~ScopedCwd(){ std::filesystem::current_path(old_); }
50+
51+
private:
52+
std::filesystem::path old_;
53+
};
54+
55+
class ScopedLang{
56+
public:
57+
explicit ScopedLang(lunar::i18n::Lang lang):
58+
old_(lunar::i18n::current_lang()){
59+
lunar::i18n::set_lang(lang);
60+
}
61+
62+
~ScopedLang(){ lunar::i18n::set_lang(old_); }
63+
64+
private:
65+
lunar::i18n::Lang old_;
66+
};
67+
68+
std::filesystem::path make_case_dir(const std::string&name){
69+
std::filesystem::path dir=std::filesystem::current_path()/name;
70+
std::error_code ec;
71+
std::filesystem::remove_all(dir,ec);
72+
ec.clear();
73+
std::filesystem::create_directories(dir,ec);
74+
if(ec){
75+
throw std::runtime_error("failed to create test dir: "+dir.string());
76+
}
77+
return dir;
78+
}
79+
80+
void write_text(const std::filesystem::path&path,const std::string&text){
81+
std::ofstream ofs(path,std::ios::binary);
82+
if(!ofs){
83+
throw std::runtime_error("failed to open file: "+path.string());
84+
}
85+
ofs<<text;
86+
}
87+
88+
}
89+
90+
TEST(InteractInitBsp, NoBspPathOffersDownloaderBeforeSeriesFallback){
91+
ScopedLang lang(lunar::i18n::Lang::En);
92+
const std::filesystem::path dir=make_case_dir("interact_no_bsp_case");
93+
{
94+
ScopedCwd cwd(dir);
95+
#if LUNAR_ENABLE_SERIES_FALLBACK
96+
ScopedCin input("\nq\nn\n");
97+
#else
98+
ScopedCin input("\nq\n");
99+
#endif
100+
ScopedCout output;
101+
InterCfg cfg;
102+
const std::string ephem=init_bsp(cfg);
103+
EXPECT_TRUE(ephem.empty());
104+
EXPECT_EQ(cfg.def_bsp,"");
105+
EXPECT_NE(output.str().find("Downloadable BSP ephemerides:"),
106+
std::string::npos);
107+
EXPECT_EQ(output.str().find("Switched to the built-in"),
108+
std::string::npos);
109+
}
110+
std::error_code ec;
111+
std::filesystem::remove_all(dir,ec);
112+
}
113+
114+
TEST(InteractMode, LanguageSwitchAppliesImmediatelyAndPersists){
115+
ScopedLang lang(lunar::i18n::Lang::Zh);
116+
const std::filesystem::path dir=make_case_dir("interact_lang_case");
117+
write_text(dir/"dummy.bsp","");
118+
{
119+
ScopedCwd cwd(dir);
120+
ScopedCin input("\n1\nl\n3\n\nq\n");
121+
ScopedCout output;
122+
int_mode();
123+
EXPECT_EQ(lunar::i18n::current_lang_code(),"en");
124+
EXPECT_NE(output.str().find("Thanks for using lunar. Exiting."),
125+
std::string::npos);
126+
}
127+
const std::string cfg_text=read_file_text(dir/CFG_FILE);
128+
EXPECT_EQ(trim(txt_value(cfg_text,"default_lang")),"en");
129+
std::error_code ec;
130+
std::filesystem::remove_all(dir,ec);
131+
}

0 commit comments

Comments
 (0)