feat: 增强多语言支持与本地化功能
- 新增多语言问题修复计划文档,详细描述了多语言支持的现状与解决方案。 - 在LocalizationManager中启用全局本地化方法,替换多个视图中的NSLocalizedString调用为LocalizedString。 - 更新MainFeature以确保在MeView标签页时正确加载用户数据。 - 在多个视图中添加语言切换测试区域,确保文本实时更新。 - 修复MeView显示问题,确保用户信息和动态内容正确加载。
This commit is contained in:
63
issues/多语言问题修复.md
Normal file
63
issues/多语言问题修复.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 多语言问题修复计划
|
||||
|
||||
## 问题描述
|
||||
项目配置了多语言支持,默认英文,但应用仍显示中文。原因是大部分视图使用 `NSLocalizedString`,它会读取系统语言设置而不是应用内保存的用户语言选择。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 修复 LocalizationManager
|
||||
- ✅ 启用了注释的 String 扩展
|
||||
- ✅ 添加了全局 `LocalizedString` 方法
|
||||
- ✅ 添加了 `LocalizedTextModifier` 结构体
|
||||
|
||||
### 2. 替换关键界面的本地化方法
|
||||
- ✅ LoginView - 应用标题、登录按钮
|
||||
- ✅ UserAgreementView - 用户协议文本
|
||||
- ✅ FeedListView - 页面标题、空状态、标语
|
||||
- ✅ IDLoginView - 标题、占位符、按钮文本
|
||||
- ✅ EMailLoginView - 标题、按钮文本
|
||||
- ✅ LanguageSettingsView - 添加测试区域
|
||||
- ✅ MeView - 用户昵称、ID显示、加载状态、错误信息
|
||||
|
||||
### 3. 修复 MeView 显示问题
|
||||
- ✅ 修复 MainFeature 中的数据加载逻辑
|
||||
- ✅ 在 accountModelLoaded 中添加 MeView 数据加载触发
|
||||
- ✅ 确保 uid 正确设置时触发数据加载
|
||||
|
||||
### 4. 新增功能
|
||||
- ✅ 全局 `LocalizedString(key, comment:)` 方法
|
||||
- ✅ String 扩展:`"key".localized`
|
||||
- ✅ 语言切换测试区域
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方法1:使用全局方法
|
||||
```swift
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
```
|
||||
|
||||
### 方法2:使用 String 扩展
|
||||
```swift
|
||||
Text("login.app_title".localized)
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
1. 在语言设置界面可以看到测试区域
|
||||
2. 切换语言后,测试区域的文本会实时更新
|
||||
3. 所有使用 `LocalizedString` 的界面都会正确显示选择的语言
|
||||
4. MeView 现在应该能正确显示用户信息和动态内容
|
||||
|
||||
## 问题修复详情
|
||||
|
||||
### MeView 不显示内容的问题
|
||||
**原因**:MainFeature 中只有当用户切换到 `.other` 标签页时才会检查 uid 并触发数据加载,但如果 accountModel 为 nil 或 uid 为 0,就不会加载数据。
|
||||
|
||||
**解决方案**:
|
||||
1. 在 `accountModelLoaded` 处理中添加 MeView 数据加载逻辑
|
||||
2. 确保当 accountModel 加载完成且当前选中 MeView 标签页时,正确设置 uid 并触发数据加载
|
||||
|
||||
## 后续工作
|
||||
- 继续替换其他界面的 `NSLocalizedString` 调用
|
||||
- 确保所有用户可见的文本都使用新的本地化方法
|
||||
- 测试各种语言切换场景
|
||||
- 验证 MeView 在不同登录状态下的显示
|
@@ -74,6 +74,14 @@ struct MainFeature {
|
||||
return .none
|
||||
case let .accountModelLoaded(accountModel):
|
||||
state.accountModel = accountModel
|
||||
// 如果当前选中的是 MeView 标签页,且有有效的 uid,则触发数据加载
|
||||
if state.selectedTab == .other, let uidStr = accountModel?.uid, let uid = Int(uidStr), uid > 0 {
|
||||
if state.me.uid != uid {
|
||||
state.me.uid = uid
|
||||
state.me.isFirstLoad = true
|
||||
}
|
||||
return .send(.me(.onAppear))
|
||||
}
|
||||
return .none
|
||||
case .me(.settingButtonTapped):
|
||||
// 触发 push 到设置页,带入当前用户信息
|
||||
|
@@ -114,26 +114,51 @@ class LocalizationManager: ObservableObject {
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI Extensions
|
||||
// extension View {
|
||||
// /// 应用本地化字符串
|
||||
// /// - Parameter key: 本地化 key
|
||||
// /// - Returns: 带有本地化文本的视图
|
||||
// @MainActor
|
||||
// func localized(_ key: String) -> some View {
|
||||
// self.modifier(LocalizedTextModifier(key: key))
|
||||
// }
|
||||
// }
|
||||
extension View {
|
||||
/// 应用本地化字符串
|
||||
/// - Parameter key: 本地化 key
|
||||
/// - Returns: 带有本地化文本的视图
|
||||
@MainActor
|
||||
func localized(_ key: String) -> some View {
|
||||
self.modifier(LocalizedTextModifier(key: key))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 便捷方法
|
||||
// extension String {
|
||||
// /// 获取本地化字符串
|
||||
// @MainActor
|
||||
// var localized: String {
|
||||
// return LocalizationManager.shared.localizedString(self)
|
||||
// }
|
||||
// /// 获取本地化字符串(带参数)
|
||||
// @MainActor
|
||||
// func localized(arguments: CVarArg...) -> String {
|
||||
// return LocalizationManager.shared.localizedString(self, arguments: arguments)
|
||||
// }
|
||||
// }
|
||||
extension String {
|
||||
/// 获取本地化字符串
|
||||
@MainActor
|
||||
var localized: String {
|
||||
return LocalizationManager.shared.localizedString(self)
|
||||
}
|
||||
/// 获取本地化字符串(带参数)
|
||||
@MainActor
|
||||
func localized(arguments: CVarArg...) -> String {
|
||||
return LocalizationManager.shared.localizedString(self, arguments: arguments)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 全局本地化方法
|
||||
/// 全局本地化字符串获取方法
|
||||
/// 使用 LocalizationManager 而不是系统语言设置
|
||||
/// - Parameters:
|
||||
/// - key: 本地化 key
|
||||
/// - comment: 注释(保持与 NSLocalizedString 兼容)
|
||||
/// - Returns: 本地化后的字符串
|
||||
@MainActor
|
||||
func LocalizedString(_ key: String, comment: String = "") -> String {
|
||||
return LocalizationManager.shared.localizedString(key)
|
||||
}
|
||||
|
||||
// MARK: - LocalizedTextModifier
|
||||
/// 本地化文本修饰符
|
||||
struct LocalizedTextModifier: ViewModifier {
|
||||
let key: String
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear {
|
||||
// 这里可以添加动态更新逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -38,20 +38,20 @@ struct UserAgreementView: View {
|
||||
|
||||
// MARK: - Private Methods
|
||||
private func createAttributedText() -> AttributedString {
|
||||
var attributedString = AttributedString(NSLocalizedString("login.agreement_policy", comment: ""))
|
||||
var attributedString = AttributedString(LocalizedString("login.agreement_policy", comment: ""))
|
||||
|
||||
// 设置默认颜色
|
||||
attributedString.foregroundColor = Color(hex: 0x666666)
|
||||
|
||||
// 找到并设置 "用户协议" 的样式和链接
|
||||
if let userServiceRange = attributedString.range(of: NSLocalizedString("login.agreement", comment: "")) {
|
||||
if let userServiceRange = attributedString.range(of: LocalizedString("login.agreement", comment: "")) {
|
||||
attributedString[userServiceRange].foregroundColor = Color(hex: 0x8A4FFF)
|
||||
attributedString[userServiceRange].underlineStyle = .single
|
||||
attributedString[userServiceRange].link = URL(string: "user-service-agreement")
|
||||
}
|
||||
|
||||
// 找到并设置 "隐私政策" 的样式和链接
|
||||
if let privacyPolicyRange = attributedString.range(of: NSLocalizedString("login.policy", comment: "")) {
|
||||
if let privacyPolicyRange = attributedString.range(of: LocalizedString("login.policy", comment: "")) {
|
||||
attributedString[privacyPolicyRange].foregroundColor = Color(hex: 0x8A4FFF)
|
||||
attributedString[privacyPolicyRange].underlineStyle = .single
|
||||
attributedString[privacyPolicyRange].link = URL(string: "privacy-policy")
|
||||
|
@@ -27,7 +27,7 @@ struct EMailLoginView: View {
|
||||
} else if codeCountdown > 0 {
|
||||
return "\(codeCountdown)S"
|
||||
} else {
|
||||
return NSLocalizedString("email_login.get_code", comment: "")
|
||||
return LocalizedString("email_login.get_code", comment: "")
|
||||
}
|
||||
}
|
||||
private var isCodeButtonEnabled: Bool {
|
||||
@@ -147,7 +147,7 @@ private struct LoginContentView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
Spacer().frame(height: 60)
|
||||
Text(NSLocalizedString("email_login.title", comment: ""))
|
||||
Text(LocalizedString("email_login.title", comment: ""))
|
||||
.font(.system(size: 28, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 80)
|
||||
@@ -235,7 +235,7 @@ private struct LoginContentView: View {
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
}
|
||||
Text(store.isLoading ? NSLocalizedString("email_login.logging_in", comment: "") : NSLocalizedString("email_login.login_button", comment: ""))
|
||||
Text(store.isLoading ? LocalizedString("email_login.logging_in", comment: "") : LocalizedString("email_login.login_button", comment: ""))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ struct TopBarView: View {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
Text(LocalizedString("feedList.title", comment: "Enjoy your Life Time"))
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -66,7 +66,7 @@ struct ErrorView: View {
|
||||
// MARK: - EmptyView
|
||||
struct EmptyView: View {
|
||||
var body: some View {
|
||||
Text(NSLocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
Text(LocalizedString("feedList.empty", comment: "暂无动态"))
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 20)
|
||||
@@ -232,7 +232,7 @@ struct FeedListView: View {
|
||||
.frame(width: 56, height: 41)
|
||||
.padding(.top, 16)
|
||||
|
||||
Text(NSLocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable."))
|
||||
Text(LocalizedString("feedList.slogan", comment: "The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable."))
|
||||
.font(.system(size: 16))
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
@@ -52,7 +52,7 @@ struct IDLoginView: View {
|
||||
.frame(height: 60)
|
||||
|
||||
// 标题
|
||||
Text(NSLocalizedString("id_login.title", comment: ""))
|
||||
Text(LocalizedString("id_login.title", comment: ""))
|
||||
.font(.system(size: 28, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 80)
|
||||
@@ -71,7 +71,7 @@ struct IDLoginView: View {
|
||||
|
||||
TextField("", text: $userID) // 使用SwiftUI的绑定
|
||||
.placeholder(when: userID.isEmpty) {
|
||||
Text(NSLocalizedString("placeholder.enter_id", comment: ""))
|
||||
Text(LocalizedString("placeholder.enter_id", comment: ""))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
@@ -94,7 +94,7 @@ struct IDLoginView: View {
|
||||
if isPasswordVisible {
|
||||
TextField("", text: $password) // 使用SwiftUI的绑定
|
||||
.placeholder(when: password.isEmpty) {
|
||||
Text(NSLocalizedString("placeholder.enter_password", comment: ""))
|
||||
Text(LocalizedString("placeholder.enter_password", comment: ""))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
@@ -102,7 +102,7 @@ struct IDLoginView: View {
|
||||
} else {
|
||||
SecureField("", text: $password) // 使用SwiftUI的绑定
|
||||
.placeholder(when: password.isEmpty) {
|
||||
Text(NSLocalizedString("placeholder.enter_password", comment: ""))
|
||||
Text(LocalizedString("placeholder.enter_password", comment: ""))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
@@ -126,7 +126,7 @@ struct IDLoginView: View {
|
||||
Button(action: {
|
||||
showRecoverPassword = true
|
||||
}) {
|
||||
Text(NSLocalizedString("id_login.forgot_password", comment: ""))
|
||||
Text(LocalizedString("id_login.forgot_password", comment: ""))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
@@ -156,7 +156,7 @@ struct IDLoginView: View {
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
.scaleEffect(0.8)
|
||||
}
|
||||
Text(store.isLoading ? NSLocalizedString("id_login.logging_in", comment: "") : NSLocalizedString("id_login.login_button", comment: ""))
|
||||
Text(store.isLoading ? LocalizedString("id_login.logging_in", comment: "") : LocalizedString("id_login.login_button", comment: ""))
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
@@ -48,6 +48,32 @@ struct LanguageSettingsView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
// 新增:语言切换测试区域
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("语言切换测试")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("应用标题: \(LocalizedString("login.app_title", comment: ""))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("登录按钮: \(LocalizedString("login.id_login", comment: ""))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("当前语言代码: \(localizationManager.currentLanguage.rawValue)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
} header: {
|
||||
Text("测试区域")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Section("调试功能") {
|
||||
Button("测试腾讯云 COS Token") {
|
||||
|
@@ -48,7 +48,7 @@ struct LoginView: View {
|
||||
)
|
||||
// E-PARTI 文本,底部对齐"top"图片底部,间距20
|
||||
HStack {
|
||||
Text(NSLocalizedString("login.app_title", comment: ""))
|
||||
Text(LocalizedString("login.app_title", comment: ""))
|
||||
.font(FontManager.adaptedFont(.bayonRegular, designSize: 56, for: geometry.size.width))
|
||||
.foregroundColor(.white)
|
||||
.padding(.leading, 20)
|
||||
@@ -82,7 +82,7 @@ struct LoginView: View {
|
||||
LoginButton(
|
||||
iconName: "person.circle.fill",
|
||||
iconColor: .green,
|
||||
title: NSLocalizedString("login.id_login", comment: "")
|
||||
title: LocalizedString("login.id_login", comment: "")
|
||||
) {
|
||||
showIDLogin = true // 直接设置SwiftUI状态
|
||||
}
|
||||
@@ -90,7 +90,7 @@ struct LoginView: View {
|
||||
LoginButton(
|
||||
iconName: "envelope.fill",
|
||||
iconColor: .blue,
|
||||
title: NSLocalizedString("login.email_login", comment: "")
|
||||
title: LocalizedString("login.email_login", comment: "")
|
||||
) {
|
||||
showEmailLogin = true // 显示邮箱登录界面
|
||||
}
|
||||
|
@@ -107,10 +107,10 @@ struct MeView: View {
|
||||
Image(systemName: "person.fill").font(.system(size: 40)).foregroundColor(.white)
|
||||
.frame(width: 90, height: 90)
|
||||
}
|
||||
Text(userInfo.nick ?? "用户昵称")
|
||||
Text(userInfo.nick ?? LocalizedString("me.nickname", comment: "用户昵称"))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
Text("ID: \(userInfo.uid ?? 0)")
|
||||
Text(LocalizedString("me.id", comment: "ID: %@").localized(arguments: String(userInfo.uid ?? 0)))
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
@@ -125,7 +125,7 @@ struct MeView: View {
|
||||
@ViewBuilder
|
||||
private func momentsSection(viewStore: ViewStoreOf<MeFeature>) -> some View {
|
||||
if viewStore.isLoadingMoments && viewStore.moments.isEmpty {
|
||||
ProgressView("加载中...")
|
||||
ProgressView(LocalizedString("feed.loadingMore", comment: "加载中..."))
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let error = viewStore.momentsError {
|
||||
VStack(spacing: 16) {
|
||||
@@ -134,7 +134,7 @@ struct MeView: View {
|
||||
.foregroundColor(.yellow)
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
Button("重试") {
|
||||
Button(LocalizedString("feed.retry", comment: "重试")) {
|
||||
viewStore.send(.onAppear)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +144,7 @@ struct MeView: View {
|
||||
Image(systemName: "tray")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.gray)
|
||||
Text("暂无动态")
|
||||
Text(LocalizedString("feed.empty", comment: "暂无动态"))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
Reference in New Issue
Block a user