mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2025-05-17 23:28:09 +08:00
添加多国语言支持
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,4 +8,5 @@ sdkconfig.old
|
||||
sdkconfig
|
||||
dependencies.lock
|
||||
.env
|
||||
releases/
|
||||
releases/
|
||||
main/assets/lang_config.h
|
@ -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}
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
34
main/assets/en-US/language.json
Normal file
34
main/assets/en-US/language.json
Normal 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"
|
||||
|
||||
}
|
||||
}
|
34
main/assets/zh/language.json
Normal file
34
main/assets/zh/language.json
Normal 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":"测试"
|
||||
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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_);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
61
scripts/gen_lang.py
Normal 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)
|
Reference in New Issue
Block a user