diff --git a/.gitignore b/.gitignore index 760157e6..db2379ba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ sdkconfig.old sdkconfig dependencies.lock .env -releases/ \ No newline at end of file +releases/ +main/assets/lang_config.h \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2590469a..bfe17fc5 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -135,3 +135,31 @@ idf_component_register(SRCS ${SOURCES} target_compile_definitions(${COMPONENT_LIB} PRIVATE BOARD_TYPE=\"${BOARD_TYPE}\" ) + +# 根据Kconfig选择语言目录 +if(CONFIG_LANGUAGE_ZH) + set(LANG_DIR "zh") +elseif(CONFIG_LANGUAGE_EN) + set(LANG_DIR "en-US") +endif() + +# 定义生成路径 +set(LANG_JSON "${CMAKE_CURRENT_SOURCE_DIR}/assets/${LANG_DIR}/language.json") +set(LANG_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/assets/lang_config.h") + +# 添加生成规则 +add_custom_command( + OUTPUT ${LANG_HEADER} + COMMAND python3 ${PROJECT_DIR}/scripts/gen_lang.py + --input "${LANG_JSON}" + --output "${LANG_HEADER}" + DEPENDS + ${LANG_JSON} + ${PROJECT_DIR}/scripts/gen_lang.py + COMMENT "Generating ${LANG_DIR} language config" +) + +# 强制建立生成依赖 +add_custom_target(lang_header ALL + DEPENDS ${LANG_HEADER} +) diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 208754df..69549510 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -6,6 +6,20 @@ config OTA_VERSION_URL help The application will access this URL to check for updates. + +choice + prompt "语言选择" + default LANGUAGE_ZH + help + Select device display language + + config LANGUAGE_ZH + bool "Chinese" + config LANGUAGE_EN + bool "English" +endchoice + + choice CONNECTION_TYPE prompt "Connection Type" default CONNECTION_TYPE_MQTT_UDP diff --git a/main/application.cc b/main/application.cc index 1d66ffb7..25fc7197 100644 --- a/main/application.cc +++ b/main/application.cc @@ -1,6 +1,7 @@ #include "application.h" #include "board.h" #include "display.h" +#include "ssd1306_display.h" #include "system_info.h" #include "ml307_ssl_transport.h" #include "audio_codec.h" @@ -9,6 +10,7 @@ #include "font_awesome_symbols.h" #include "iot/thing_manager.h" #include "assets/zh/binary.h" +#include "assets/lang_config.h" #include #include @@ -117,7 +119,7 @@ void Application::CheckNewVersion() { // No new version, mark the current version as valid ota_.MarkCurrentVersionValid(); - display->ShowNotification("版本 " + ota_.GetCurrentVersion()); + display->ShowNotification(Lang::Strings::VERSION + " " + ota_.GetCurrentVersion()); if (ota_.HasActivationCode()) { // Activation code is valid @@ -218,7 +220,7 @@ void Application::ToggleChatState() { if (device_state_ == kDeviceStateIdle) { SetDeviceState(kDeviceStateConnecting); if (!protocol_->OpenAudioChannel()) { - Alert("ERROR", "无法建立音频通道", "sad"); + Alert("ERROR", Lang::Strings::UNABLE_TO_ESTABLISH_AUDIO_CHANNEL, "sad"); SetDeviceState(kDeviceStateIdle); return; } @@ -252,7 +254,7 @@ void Application::StartListening() { SetDeviceState(kDeviceStateConnecting); if (!protocol_->OpenAudioChannel()) { SetDeviceState(kDeviceStateIdle); - Alert("ERROR", "无法建立音频通道", "sad"); + Alert("ERROR", Lang::Strings::UNABLE_TO_ESTABLISH_AUDIO_CHANNEL, "sad"); return; } } @@ -326,7 +328,7 @@ void Application::Start() { board.StartNetwork(); // Initialize the protocol - display->SetStatus("加载协议..."); + display->SetStatus(Lang::Strings::LOADING_PROTOCOL + "..."); #ifdef CONFIG_CONNECTION_TYPE_WEBSOCKET protocol_ = std::make_unique(); #else @@ -662,18 +664,18 @@ void Application::SetDeviceState(DeviceState state) { switch (state) { case kDeviceStateUnknown: case kDeviceStateIdle: - display->SetStatus("待命"); + display->SetStatus(Lang::Strings::STANDING_BY); display->SetEmotion("neutral"); #ifdef CONFIG_USE_AUDIO_PROCESSING audio_processor_.Stop(); #endif break; case kDeviceStateConnecting: - display->SetStatus("连接中..."); + display->SetStatus(Lang::Strings::CONNECTING+"..."); display->SetChatMessage("system", ""); break; case kDeviceStateListening: - display->SetStatus("聆听中..."); + display->SetStatus(Lang::Strings::LISTENING+"..."); display->SetEmotion("neutral"); ResetDecoder(); opus_encoder_->ResetState(); @@ -683,7 +685,7 @@ void Application::SetDeviceState(DeviceState state) { UpdateIotStates(); break; case kDeviceStateSpeaking: - display->SetStatus("说话中..."); + display->SetStatus(Lang::Strings::SPEAKING+"..."); ResetDecoder(); codec->EnableOutput(true); #if CONFIG_USE_AUDIO_PROCESSING diff --git a/main/assets/en-US/language.json b/main/assets/en-US/language.json new file mode 100644 index 00000000..a800ac12 --- /dev/null +++ b/main/assets/en-US/language.json @@ -0,0 +1,34 @@ +{ + "language": { + "type" :"en" + }, + "strings": { + "VERSION": "Version", + "LOADING_PROTOCOL":"Loading Protocol", + "INITIALIZING":"Initializing", + "NOTICE":"Notice", + + "STANDING_BY":"Standing By", + "CONNECT":"Connect", + "CONNECTING":"Connecting", + "CONNECTION_SUCCESSFUL":"Connection Successful", + + "LISTENING":"Listening", + "SPEAKING":"Speaking", + + "UNABLE_TO_CONNECT_TO_SERVICE":"Unable to connect to service", + "WAITING_FOR_RESPONSE_TIMEOUT":"Waiting for response timeout", + "SENDING_FAILED_PLEASE_CHECK_THE_NETWORK":"Sending failed, please check the network", + + + + "CONNECT_MOBILE_PHONE_TO_HOTSPOT":"Connect mobile phone to hotspot", + "ACCESS_VIA_BROWSER":"Access via browser", + "WIFI_CONFIGURATION_MODE":"Wi-Fi Configuration Mode", + "SCANNING_WIFI":"Scanning Wi-Fi", + + "UNABLE_TO_ESTABLISH_AUDIO_CHANNEL": "Unable to establish audio channel", + "TEST":"Test" + + } +} \ No newline at end of file diff --git a/main/assets/zh/language.json b/main/assets/zh/language.json new file mode 100644 index 00000000..ad8eea8f --- /dev/null +++ b/main/assets/zh/language.json @@ -0,0 +1,34 @@ +{ + "language": { + "type" :"zh" + }, + "strings": { + "VERSION": "版本", + "LOADING_PROTOCOL":"加载协议", + "INITIALIZING":"正在初始化", + "NOTICE":"通知", + + "STANDING_BY":"待命", + "CONNECT":"连接", + "CONNECTING":"连接中", + "CONNECTION_SUCCESSFUL":"连接成功", + + "LISTENING":"聆听中", + "SPEAKING":"说话中", + + "UNABLE_TO_CONNECT_TO_SERVICE":"无法连接服务", + "WAITING_FOR_RESPONSE_TIMEOUT":"等待响应超时", + "SENDING_FAILED_PLEASE_CHECK_THE_NETWORK":"发送失败,请检查网络", + + + + "CONNECT_MOBILE_PHONE_TO_HOTSPOT":"手机连接热点", + "ACCESS_VIA_BROWSER":"浏览器访问", + "WIFI_CONFIGURATION_MODE":"配网模式", + "SCANNING_WIFI":"扫描 WIFI", + + "UNABLE_TO_ESTABLISH_AUDIO_CHANNEL": "无法建立音频通道", + "TEST":"测试" + + } +} diff --git a/main/boards/common/wifi_board.cc b/main/boards/common/wifi_board.cc index 20847000..f0755411 100644 --- a/main/boards/common/wifi_board.cc +++ b/main/boards/common/wifi_board.cc @@ -20,6 +20,7 @@ #include #include #include +#include "assets/lang_config.h" static const char *TAG = "WifiBoard"; @@ -45,14 +46,14 @@ void WifiBoard::EnterWifiConfigMode() { wifi_ap.Start(); // 显示 WiFi 配置 AP 的 SSID 和 Web 服务器 URL - std::string hint = "手机连接热点 "; + std::string hint = Lang::Strings::CONNECT_MOBILE_PHONE_TO_HOTSPOT + " "; hint += wifi_ap.GetSsid(); - hint += "\n浏览器访问 "; + hint += "\n"+ Lang::Strings::ACCESS_VIA_BROWSER + " "; hint += wifi_ap.GetWebServerUrl(); hint += "\n\n"; // 播报配置 WiFi 的提示 - application.Alert("配网模式", hint, "", std::string_view(p3_wificonfig_start, p3_wificonfig_end - p3_wificonfig_start)); + application.Alert(Lang::Strings::WIFI_CONFIGURATION_MODE, hint, "", std::string(p3_wificonfig_start, p3_wificonfig_end - p3_wificonfig_start)); // Wait forever until reset after configuration while (true) { @@ -82,15 +83,15 @@ void WifiBoard::StartNetwork() { auto& wifi_station = WifiStation::GetInstance(); wifi_station.OnScanBegin([this]() { auto display = Board::GetInstance().GetDisplay(); - display->ShowNotification("扫描 WiFi...", 30000); + display->ShowNotification(Lang::Strings::SCANNING_WIFI, 30000); }); wifi_station.OnConnect([this](const std::string& ssid) { auto display = Board::GetInstance().GetDisplay(); - display->ShowNotification(std::string("连接 ") + ssid + "...", 30000); + display->ShowNotification(std::string(Lang::Strings::CONNECT + " ") + ssid + "...", 30000); }); wifi_station.OnConnected([this](const std::string& ssid) { auto display = Board::GetInstance().GetDisplay(); - display->ShowNotification(std::string("已连接 ") + ssid); + display->ShowNotification(std::string(Lang::Strings::CONNECTION_SUCCESSFUL) + ssid); }); wifi_station.Start(); @@ -171,7 +172,7 @@ void WifiBoard::ResetWifiConfiguration() { Settings settings("wifi", true); settings.SetInt("force_ap", 1); } - GetDisplay()->ShowNotification("进入配网模式..."); + GetDisplay()->ShowNotification("Enter the network configuration mode..."); vTaskDelay(pdMS_TO_TICKS(1000)); // Reboot the device esp_restart(); diff --git a/main/display/lcd_display.cc b/main/display/lcd_display.cc index 68e13fe3..7211735b 100644 --- a/main/display/lcd_display.cc +++ b/main/display/lcd_display.cc @@ -7,6 +7,7 @@ #include #include #include +#include "assets/lang_config.h" #include "board.h" @@ -256,13 +257,13 @@ void LcdDisplay::SetupUI() { notification_label_ = lv_label_create(status_bar_); lv_obj_set_flex_grow(notification_label_, 1); lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); - lv_label_set_text(notification_label_, "通知"); + lv_label_set_text(notification_label_, (Lang::Strings::NOTICE).c_str()); lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); status_label_ = lv_label_create(status_bar_); lv_obj_set_flex_grow(status_label_, 1); lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_label_set_text(status_label_, "正在初始化"); + lv_label_set_text(status_label_,(Lang::Strings::INITIALIZING + "...").c_str()); lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0); mute_label_ = lv_label_create(status_bar_); diff --git a/main/display/ssd1306_display.cc b/main/display/ssd1306_display.cc index 441d296b..6ce5dba6 100644 --- a/main/display/ssd1306_display.cc +++ b/main/display/ssd1306_display.cc @@ -6,6 +6,7 @@ #include #include #include +#include "assets/lang_config.h" #define TAG "Ssd1306Display" @@ -220,12 +221,12 @@ void Ssd1306Display::SetupUI_128x64() { notification_label_ = lv_label_create(status_bar_); lv_obj_set_flex_grow(notification_label_, 1); lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0); - lv_label_set_text(notification_label_, "通知"); + lv_label_set_text(notification_label_, (Lang::Strings::NOTICE).c_str()); lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); status_label_ = lv_label_create(status_bar_); lv_obj_set_flex_grow(status_label_, 1); - lv_label_set_text(status_label_, "正在初始化"); + lv_label_set_text(status_label_,(Lang::Strings::INITIALIZING + "...").c_str()); lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0); mute_label_ = lv_label_create(status_bar_); @@ -295,10 +296,10 @@ void Ssd1306Display::SetupUI_128x32() { status_label_ = lv_label_create(status_bar_); lv_obj_set_style_pad_left(status_label_, 2, 0); - lv_label_set_text(status_label_, "正在初始化"); + lv_label_set_text(status_label_,(Lang::Strings::INITIALIZING + "...").c_str()); notification_label_ = lv_label_create(status_bar_); - lv_label_set_text(notification_label_, "通知"); + lv_label_set_text(notification_label_, (Lang::Strings::NOTICE).c_str()); lv_obj_set_style_pad_left(notification_label_, 2, 0); lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN); diff --git a/main/protocols/mqtt_protocol.cc b/main/protocols/mqtt_protocol.cc index 669be3d2..caef82db 100644 --- a/main/protocols/mqtt_protocol.cc +++ b/main/protocols/mqtt_protocol.cc @@ -8,6 +8,7 @@ #include #include #include +#include "assets/lang_config.h" #define TAG "MQTT" @@ -87,7 +88,7 @@ bool MqttProtocol::StartMqttClient() { if (!mqtt_->Connect(endpoint_, 8883, client_id_, username_, password_)) { ESP_LOGE(TAG, "Failed to connect to endpoint"); if (on_network_error_ != nullptr) { - on_network_error_("无法连接服务"); + on_network_error_(Lang::Strings::UNABLE_TO_CONNECT_TO_SERVICE); } return false; } @@ -103,7 +104,7 @@ void MqttProtocol::SendText(const std::string& text) { if (!mqtt_->Publish(publish_topic_, text)) { ESP_LOGE(TAG, "Failed to publish message"); if (on_network_error_ != nullptr) { - on_network_error_("发送失败,请检查网络"); + on_network_error_(Lang::Strings::SENDING_FAILED_PLEASE_CHECK_THE_NETWORK); } } } @@ -178,7 +179,7 @@ bool MqttProtocol::OpenAudioChannel() { if (!(bits & MQTT_PROTOCOL_SERVER_HELLO_EVENT)) { ESP_LOGE(TAG, "Failed to receive server hello"); if (on_network_error_ != nullptr) { - on_network_error_("等待响应超时"); + on_network_error_(Lang::Strings::WAITING_FOR_RESPONSE_TIMEOUT); } return false; } diff --git a/main/protocols/websocket_protocol.cc b/main/protocols/websocket_protocol.cc index 4964dbc0..7659e5df 100644 --- a/main/protocols/websocket_protocol.cc +++ b/main/protocols/websocket_protocol.cc @@ -7,6 +7,7 @@ #include #include #include +#include "assets/lang_config.h" #define TAG "WS" @@ -98,7 +99,7 @@ bool WebsocketProtocol::OpenAudioChannel() { if (!websocket_->Connect(url.c_str())) { ESP_LOGE(TAG, "Failed to connect to websocket server"); if (on_network_error_ != nullptr) { - on_network_error_("无法连接服务"); + on_network_error_(Lang::Strings::UNABLE_TO_CONNECT_TO_SERVICE); } return false; } @@ -119,7 +120,7 @@ bool WebsocketProtocol::OpenAudioChannel() { if (!(bits & WEBSOCKET_PROTOCOL_SERVER_HELLO_EVENT)) { ESP_LOGE(TAG, "Failed to receive server hello"); if (on_network_error_ != nullptr) { - on_network_error_("等待响应超时"); + on_network_error_(Lang::Strings::WAITING_FOR_RESPONSE_TIMEOUT); } return false; } diff --git a/scripts/gen_lang.py b/scripts/gen_lang.py new file mode 100644 index 00000000..9885acc0 --- /dev/null +++ b/scripts/gen_lang.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +import argparse +import json +import os + +HEADER_TEMPLATE = """// Auto-generated language config +#pragma once + +#include +#include + +namespace Lang {{ + // 语言元数据 + constexpr std::string_view CODE_VIEW = "{lang_code}"; + const std::string CODE = std::string(CODE_VIEW); + + // 字符串资源 + namespace Strings {{ +{strings_view} +{strings_string} + }} +}} +""" + +def generate_header(input_path, output_path): + with open(input_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 验证数据结构 + if 'language' not in data or 'strings' not in data: + raise ValueError("Invalid JSON structure") + + lang_code = data['language']['type'] + + # 生成字符串常量 + strings_view = [] + strings_string = [] + for key, value in data['strings'].items(): + value = value.replace('"', '\\"') + strings_view.append(f' constexpr std::string_view {key.upper()}_VIEW = "{value}";') + strings_string.append(f' const std::string {key.upper()} = std::string({key.upper()}_VIEW);') + + # 填充模板 + content = HEADER_TEMPLATE.format( + lang_code=lang_code, + strings_view="\n".join(sorted(strings_view)), + strings_string="\n".join(sorted(strings_string)) + ) + + # 写入文件 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, 'w', encoding='utf-8') as f: + f.write(content) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input", required=True, help="输入JSON文件路径") + parser.add_argument("--output", required=True, help="输出头文件路径") + args = parser.parse_args() + + generate_header(args.input, args.output) \ No newline at end of file