添加多国语言支持

This commit is contained in:
VinJay
2025-02-18 19:33:07 +08:00
parent 140aab8999
commit d5594d01a3
12 changed files with 206 additions and 27 deletions

3
.gitignore vendored
View File

@ -8,4 +8,5 @@ sdkconfig.old
sdkconfig
dependencies.lock
.env
releases/
releases/
main/assets/lang_config.h

View File

@ -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}
)

View File

@ -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

View File

@ -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 <cstring>
#include <esp_log.h>
@ -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<WebsocketProtocol>();
#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

View File

@ -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"
}
}

View File

@ -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":"测试"
}
}

View File

@ -20,6 +20,7 @@
#include <wifi_station.h>
#include <wifi_configuration_ap.h>
#include <ssid_manager.h>
#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();

View File

@ -7,6 +7,7 @@
#include <vector>
#include <esp_lvgl_port.h>
#include <esp_timer.h>
#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_);

View File

@ -6,6 +6,7 @@
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_vendor.h>
#include <esp_lvgl_port.h>
#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);

View File

@ -8,6 +8,7 @@
#include <ml307_udp.h>
#include <cstring>
#include <arpa/inet.h>
#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;
}

View File

@ -7,6 +7,7 @@
#include <cJSON.h>
#include <esp_log.h>
#include <arpa/inet.h>
#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;
}

61
scripts/gen_lang.py Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
import argparse
import json
import os
HEADER_TEMPLATE = """// Auto-generated language config
#pragma once
#include <string>
#include <string_view>
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)