Эх сурвалжийг харах

Add Vietnamese localization and locale-aware date formatting.

Include vi.lproj strings, register the language in the picker and project, and format calendar and schedule dates using the app-selected locale.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 сар өмнө
parent
commit
a6a0dc2d5a

+ 1 - 0
Info.plist

@@ -18,6 +18,7 @@
 		<string>ko</string>
 		<string>pt-BR</string>
 		<string>ru</string>
+		<string>vi</string>
 		<string>zh-Hans</string>
 		<string>zh-Hant</string>
 	</array>

+ 1 - 0
meetings_app.xcodeproj/project.pbxproj

@@ -106,6 +106,7 @@
 				ko,
 				"pt-BR",
 				ru,
+				vi,
 				"zh-Hans",
 				"zh-Hant",
 			);

+ 29 - 1
meetings_app/Localization/AppLanguageManager.swift

@@ -8,7 +8,7 @@ enum AppLanguageManager {
     private static let defaultsKey = "settings.appLanguageCode"
 
     /// UI languages shown in Settings → Language (order preserved).
-    private static let supportedLanguageCodes = ["en", "fr", "fr-CA", "es-ES", "es-MX", "de", "sv", "ko", "pt-BR", "ru", "zh-Hans", "zh-Hant"]
+    private static let supportedLanguageCodes = ["en", "fr", "fr-CA", "es-ES", "es-MX", "de", "sv", "ko", "pt-BR", "ru", "vi", "zh-Hans", "zh-Hant"]
 
     /// `nil` follows the system language; otherwise uses the selected `.lproj` code.
     static var selectedLanguageCode: String? {
@@ -48,6 +48,32 @@ enum AppLanguageManager {
         return .current
     }
 
+    /// User-facing dates/times respect Settings → Language (not only the system locale).
+    static func dateFormatter(
+        dateFormat: String? = nil,
+        dateStyle: DateFormatter.Style = .none,
+        timeStyle: DateFormatter.Style = .none
+    ) -> DateFormatter {
+        let formatter = DateFormatter()
+        formatter.locale = formattingLocale
+        formatter.timeZone = TimeZone.current
+        if let dateFormat {
+            formatter.dateFormat = dateFormat
+        } else {
+            formatter.dateStyle = dateStyle
+            formatter.timeStyle = timeStyle
+        }
+        return formatter
+    }
+
+    static func localizedDateString(
+        from date: Date,
+        dateStyle: DateFormatter.Style,
+        timeStyle: DateFormatter.Style
+    ) -> String {
+        dateFormatter(dateStyle: dateStyle, timeStyle: timeStyle).string(from: date)
+    }
+
     /// Localized product name (splash, share sheet, etc.).
     static var localizedAppDisplayName: String {
         "AI Companion for Meet".localized
@@ -75,6 +101,8 @@ enum AppLanguageManager {
             return "Português (Brasil)"
         case "ru":
             return "Русский"
+        case "vi":
+            return "Tiếng Việt"
         case "zh-Hans":
             return "简体中文"
         case "zh-Hant":

+ 12 - 33
meetings_app/ViewController.swift

@@ -2423,7 +2423,7 @@ private extension ViewController {
     private func localeCurrencyText(for amount: Decimal) -> String {
         let formatter = NumberFormatter()
         formatter.numberStyle = .currency
-        formatter.locale = Locale.current
+        formatter.locale = AppLanguageManager.formattingLocale
 
         if formatter.currencyCode == nil {
             formatter.currencyCode = "USD"
@@ -3269,7 +3269,7 @@ private extension ViewController {
         stack.spacing = 8
 
         let title = textLabel("Recording in progress: %@".localized(with: session.title), font: NSFont.systemFont(ofSize: 15, weight: .semibold), color: palette.textPrimary)
-        let started = DateFormatter.localizedString(from: session.startedAt, dateStyle: .medium, timeStyle: .short)
+        let started = AppLanguageManager.localizedDateString(from: session.startedAt, dateStyle: .medium, timeStyle: .short)
         let status = textLabel("Started: %@".localized(with: started), font: typography.fieldLabel, color: palette.textSecondary)
         let stopButton = NSButton(title: "Stop Recording".localized, target: self, action: #selector(aiCompanionStopRecordingTapped(_:)))
         stopButton.translatesAutoresizingMaskIntoConstraints = false
@@ -3312,11 +3312,11 @@ private extension ViewController {
         title.maximumNumberOfLines = 2
         title.lineBreakMode = .byTruncatingTail
 
-        let dateText = DateFormatter.localizedString(from: recording.endedAt, dateStyle: .medium, timeStyle: .none)
+        let dateText = AppLanguageManager.localizedDateString(from: recording.endedAt, dateStyle: .medium, timeStyle: .none)
         let dateLabel = textLabel("Date: %@".localized(with: dateText), font: typography.fieldLabel, color: palette.textSecondary)
         dateLabel.alignment = .left
 
-        let savedTimeText = DateFormatter.localizedString(from: recording.endedAt, dateStyle: .none, timeStyle: .short)
+        let savedTimeText = AppLanguageManager.localizedDateString(from: recording.endedAt, dateStyle: .none, timeStyle: .short)
         let savedTimeLabel = textLabel("Saved time: %@".localized(with: savedTimeText), font: typography.fieldLabel, color: palette.textMuted)
         savedTimeLabel.alignment = .left
 
@@ -8616,17 +8616,13 @@ private extension ViewController {
     }
 
     private func calendarMonthTitleText(for monthAnchor: Date) -> String {
-        let f = DateFormatter()
-        f.locale = Locale.current
-        f.timeZone = TimeZone.current
-        f.dateFormat = "MMMM yyyy"
-        return f.string(from: monthAnchor)
+        AppLanguageManager.dateFormatter(dateFormat: "MMMM yyyy").string(from: monthAnchor)
     }
 
     private func calendarWeekdaySymbolsStartingAtFirstWeekday() -> [String] {
         // Align weekday header to Calendar.current.firstWeekday
         let calendar = Calendar.current
-        var symbols = DateFormatter().veryShortWeekdaySymbols ?? ["S", "M", "T", "W", "T", "F", "S"]
+        var symbols = AppLanguageManager.dateFormatter().veryShortWeekdaySymbols ?? ["S", "M", "T", "W", "T", "F", "S"]
         // veryShortWeekdaySymbols starts with Sunday in most locales; rotate to firstWeekday.
         let first = max(1, min(7, calendar.firstWeekday)) // 1..7
         let shift = (first - 1) % 7
@@ -8826,10 +8822,7 @@ private extension ViewController {
             .filter { $0.startDate >= selectedDay && $0.startDate < nextDay }
             .sorted(by: { $0.startDate < $1.startDate })
 
-        let f = DateFormatter()
-        f.locale = Locale.current
-        f.timeZone = TimeZone.current
-        f.dateFormat = "EEE, d MMM"
+        let f = AppLanguageManager.dateFormatter(dateFormat: "EEE, d MMM")
 
         if meetings.isEmpty {
             calendarPageDaySummaryLabel?.stringValue = hasGoogleSessionAvailable() == false
@@ -9603,20 +9596,12 @@ private extension ViewController {
 
     private func scheduleTimeText(for meeting: ScheduledMeeting) -> String {
         if meeting.isAllDay { return "All day".localized }
-        let f = DateFormatter()
-        f.locale = Locale.current
-        f.timeZone = TimeZone.current
-        f.dateStyle = .none
-        f.timeStyle = .short
+        let f = AppLanguageManager.dateFormatter(dateStyle: .none, timeStyle: .short)
         return "\(f.string(from: meeting.startDate)) - \(f.string(from: meeting.endDate))"
     }
 
     private func scheduleDayText(for meeting: ScheduledMeeting) -> String {
-        let f = DateFormatter()
-        f.locale = Locale.current
-        f.timeZone = TimeZone.current
-        f.dateFormat = "EEE, d MMM"
-        return f.string(from: meeting.startDate)
+        AppLanguageManager.dateFormatter(dateFormat: "EEE, d MMM").string(from: meeting.startDate)
     }
 
     private func scheduleDurationText(for meeting: ScheduledMeeting) -> String {
@@ -9636,11 +9621,7 @@ private extension ViewController {
         }
 
         let day = Calendar.current.startOfDay(for: first.startDate)
-        let f = DateFormatter()
-        f.locale = Locale.current
-        f.timeZone = TimeZone.current
-        f.dateFormat = "EEEE, d MMM"
-        return f.string(from: day)
+        return AppLanguageManager.dateFormatter(dateFormat: "EEEE, d MMM").string(from: day)
     }
 
     private func openMeetingURL(_ url: URL, meeting: ScheduledMeeting? = nil) {
@@ -9943,11 +9924,9 @@ private extension ViewController {
     }
 
     private func publishWidgetMeetingsSnapshot(from meetings: [ScheduledMeeting]) {
-        let formatter = DateFormatter()
-        formatter.dateFormat = "EEE, h:mm a"
-        formatter.locale = Locale.current
+        let formatter = AppLanguageManager.dateFormatter(dateFormat: "EEE, h:mm a")
         let payload = meetings.prefix(widgetSnapshotLimit).map { meeting in
-            let endText = DateFormatter.localizedString(from: meeting.endDate, dateStyle: .none, timeStyle: .short)
+            let endText = AppLanguageManager.localizedDateString(from: meeting.endDate, dateStyle: .none, timeStyle: .short)
             return WidgetMeetingSnapshot(
                 id: meeting.id,
                 title: meeting.title,

+ 4 - 0
meetings_app/vi.lproj/InfoPlist.strings

@@ -0,0 +1,4 @@
+"CFBundleDisplayName" = "Trợ lý AI cho Google Meet";
+"CFBundleName" = "Trợ lý AI cho Google Meet";
+"NSMicrophoneUsageDescription" = "Với sự đồng ý của bạn, ứng dụng sẽ ghi âm cuộc họp trên Mac này và hiển thị trong Trợ lý AI sau cuộc họp.";
+"NSSpeechRecognitionUsageDescription" = "Ứng dụng này chuyển đổi âm thanh cuộc họp đã lưu thành bản ghi chép văn bản cho Trợ lý AI.";

+ 452 - 0
meetings_app/vi.lproj/Localizable.strings

@@ -0,0 +1,452 @@
+/* AppBrand.name ("Meetings") được bỏ qua có chủ đích — tên ứng dụng giữ nguyên trong mã nguồn. */
+"AI Companion for Meet" = "Trợ lý AI cho Google Meet";
+
+/* MARK: - Chung */
+"OK" = "OK";
+"Cancel" = "Hủy";
+"Continue" = "Tiếp tục";
+"Apply" = "Áp dụng";
+"Reset" = "Đặt lại";
+"Save" = "Lưu";
+"Copy" = "Sao chép";
+"Copied" = "Đã sao chép";
+"Close" = "Đóng";
+"Join" = "Tham gia";
+"None" = "Không có";
+"Link" = "Liên kết";
+"Stop" = "Dừng";
+"Address" = "Địa chỉ";
+"Title" = "Tiêu đề";
+"Notes" = "Ghi chú";
+"Start" = "Bắt đầu";
+"Premium" = "Premium";
+"Popular" = "Phổ biến";
+"Browser" = "Trình duyệt";
+"Meeting" = "Cuộc họp";
+"Error" = "Lỗi";
+"Success" = "Thành công";
+
+/* MARK: - Thanh trạng thái */
+"Meetings status menu" = "Menu trạng thái cuộc họp";
+"Open App" = "Mở ứng dụng";
+"Join Meetings" = "Tham gia cuộc họp";
+"Schedule" = "Lên lịch";
+"Calendar" = "Lịch";
+"Widgets" = "Widget";
+"AI Companion" = "Trợ lý AI";
+"Settings" = "Cài đặt";
+"Top meetings" = "Cuộc họp được ghim";
+"No upcoming meetings" = "Không có cuộc họp sắp tới";
+"Sign Out" = "Đăng xuất";
+"Sign In" = "Đăng nhập";
+"Quit" = "Thoát";
+
+/* MARK: - Màn hình khởi động */
+"Preparing your meetings workspace" = "Đang chuẩn bị không gian làm việc cuộc họp";
+"CONNECTING SERVICES" = "ĐANG KẾT NỐI DỊCH VỤ";
+"READY" = "SẴN SÀNG";
+"Plan smart, achieve more" = "Lên kế hoạch thông minh, đạt được nhiều hơn";
+
+/* MARK: - Widget */
+"Add Widget to Desktop" = "Thêm widget vào màn hình nền";
+"Choose a size and add your Google Meet widget." = "Chọn kích thước và thêm widget Google Meet của bạn.";
+"Tip: Right-click a desktop widget to remove it." = "Mẹo: nhấp chuột phải vào widget trên màn hình nền để xóa.";
+"Removed %@ widget from desktop." = "Đã xóa widget %@ khỏi màn hình nền.";
+"Added %@ widget from desktop." = "Đã thêm widget %@ từ màn hình nền.";
+"Widgets are Premium only. Upgrade to add widgets." = "Widget chỉ dành cho Premium. Nâng cấp để thêm widget.";
+"Remove Widget" = "Xóa widget";
+"Small" = "Nhỏ";
+"Medium" = "Vừa";
+"Quick actions" = "Thao tác nhanh";
+"Open Meet" = "Mở Google Meet";
+"Upcoming meetings" = "Cuộc họp sắp tới";
+"Sign in to see upcoming meetings" = "Đăng nhập để xem cuộc họp sắp tới";
+"Sign in with Google" = "Đăng nhập bằng tài khoản Google";
+"Google Meet" = "Google Meet";
+"Open and join quickly" = "Mở và tham gia nhanh";
+"Refresh" = "Làm mới";
+
+/* MARK: - Thanh bên và điều hướng */
+"Planning" = "Lên kế hoạch";
+"Additional" = "Bổ sung";
+"Get Premium" = "Nhận Premium";
+"Manage Subscription" = "Quản lý đăng ký";
+"App for Google Meet" = "Ứng dụng cho Google Meet";
+
+/* MARK: - Tham gia cuộc họp */
+"New Instant Meet" = "Cuộc họp tức thì mới";
+"Start instant Meet in more section with\nGoogle Meet meet." = "Bắt đầu cuộc họp Google Meet tức thì trong phần Thêm\nvới Google Meet.";
+"Join with Link" = "Tham gia bằng liên kết";
+"Code or meet.google.com/…" = "Mã hoặc meet.google.com/…";
+"Join with ID" = "Tham gia bằng ID";
+"Join with URL" = "Tham gia bằng URL";
+"Meeting URL" = "URL cuộc họp";
+"Enter meeting URL..." = "Nhập URL cuộc họp...";
+"Quick Join Meeting" = "Tham gia cuộc họp nhanh";
+"Invalid Meet link" = "Liên kết Google Meet không hợp lệ";
+"Enter a valid Google Meet link or meeting code (for example nkd-grps-duv, meet.google.com/nkd-grps-duv, or https://meet.google.com/nkd-grps-duv)." = "Nhập liên kết Google Meet hoặc mã cuộc họp hợp lệ (ví dụ: nkd-grps-duv, meet.google.com/nkd-grps-duv hoặc https://meet.google.com/nkd-grps-duv).";
+"Instant Meeting" = "Cuộc họp tức thì";
+"Connect Google to start Instant Meeting" = "Kết nối tài khoản Google để bắt đầu cuộc họp tức thì";
+"To auto-stop recording when the meeting ends, connect your Google account first." = "Để tự động dừng ghi âm khi cuộc họp kết thúc, hãy kết nối tài khoản Google trước.";
+"Could not create Instant Meeting" = "Không thể tạo cuộc họp tức thì";
+"Free trial used. Upgrade to continue." = "Đã dùng hết bản dùng thử miễn phí. Nâng cấp để tiếp tục.";
+
+/* MARK: - Duyệt web */
+"Browse the web" = "Duyệt web";
+"Enter a web address (for example meet.google.com)." = "Nhập địa chỉ web (ví dụ: meet.google.com).";
+"Invalid address" = "Địa chỉ không hợp lệ";
+"Enter a valid http or https URL." = "Nhập URL http hoặc https hợp lệ.";
+"Browse" = "Duyệt";
+"Quick links" = "Liên kết nhanh";
+"Zoom help" = "Trợ giúp Zoom";
+"https://example.com or example.com" = "https://example.com hoặc example.com";
+"Enter a valid web address, for example https://example.com" = "Nhập địa chỉ web hợp lệ, ví dụ https://example.com";
+"Address not allowed" = "Địa chỉ không được phép";
+"This URL is not permitted with the current in-app browser policy (whitelist)." = "URL này không được phép theo chính sách trình duyệt trong ứng dụng hiện tại (danh sách trắng).";
+"Unable to load page" = "Không thể tải trang";
+"Could not load this page in the in-app browser.\n\n%@" = "Không thể tải trang này trong trình duyệt của ứng dụng.\n\n%@";
+"Page stopped loading" = "Trang đã dừng tải";
+"Unable to open browser" = "Không thể mở trình duyệt";
+"Unable to Open Safari" = "Không thể mở Safari";
+
+/* MARK: - Lên lịch và lịch */
+"All" = "Tất cả";
+"Today" = "Hôm nay";
+"This week" = "Tuần này";
+"This month" = "Tháng này";
+"Previous week" = "Tuần trước";
+"Previous month" = "Tháng trước";
+"Custom range" = "Khoảng tùy chỉnh";
+"No meetings match the selected filters." = "Không có cuộc họp nào khớp với bộ lọc đã chọn.";
+"Adjust the date range and tap Apply." = "Điều chỉnh khoảng ngày và nhấn Áp dụng.";
+"Premium required. Get Premium now to unlock Calendar." = "Cần Premium. Nhận Premium ngay để mở khóa Lịch.";
+"Premium required. Click to open paywall." = "Cần Premium. Nhấn để mở màn hình thanh toán.";
+"Get Premium to see events" = "Nhận Premium để xem sự kiện";
+"Connect Google" = "Kết nối tài khoản Google";
+"Sign in with Google first to schedule a meeting from Calendar." = "Đăng nhập bằng tài khoản Google trước để lên lịch cuộc họp từ Lịch.";
+"Select date" = "Chọn ngày";
+"Choose a date to schedule your meeting." = "Chọn ngày để lên lịch cuộc họp.";
+"Google schedule" = "Lịch Google";
+"To show scheduled meetings, connect your Google account. You’ll need a Google OAuth client ID (Desktop) and a redirect URI matching the app’s callback scheme." = "Để hiển thị các cuộc họp đã lên lịch, hãy kết nối tài khoản Google. Bạn cần ID ứng dụng khách Google OAuth (Desktop) và URI chuyển hướng khớp với lược đồ callback của ứng dụng.";
+"Actions" = "Thao tác";
+"Schedule meeting" = "Lên lịch cuộc họp";
+"Team sync" = "Đồng bộ nhóm";
+"Duration (min)" = "Thời lượng (phút)";
+"Refresh saved meetings" = "Làm mới cuộc họp đã lưu";
+
+/* MARK: - Cài đặt */
+"Manage appearance, account, and app options." = "Quản lý giao diện, tài khoản và tùy chọn ứng dụng.";
+"Appearance" = "Giao diện";
+"Account" = "Tài khoản";
+"App" = "Ứng dụng";
+"Notifications" = "Thông báo";
+"Help & Legal" = "Trợ giúp và Pháp lý";
+"Dark Mode" = "Chế độ tối";
+"Language" = "Ngôn ngữ";
+"System Language" = "Ngôn ngữ hệ thống";
+"Rate Us" = "Đánh giá ứng dụng";
+"Share App" = "Chia sẻ ứng dụng";
+"Upgrade" = "Nâng cấp";
+"Privacy Policy" = "Chính sách quyền riêng tư";
+"Support" = "Hỗ trợ";
+"Terms of Services" = "Điều khoản dịch vụ";
+"AI Language 1" = "Ngôn ngữ AI 1";
+"AI Language 2" = "Ngôn ngữ AI 2";
+"Google account connected" = "Đã kết nối tài khoản Google";
+"Google account not connected" = "Chưa kết nối tài khoản Google";
+"Signed in" = "Đã đăng nhập";
+"Sign in to sync your meetings and calendar." = "Đăng nhập để đồng bộ cuộc họp và lịch của bạn.";
+"Log out" = "Đăng xuất";
+"Check out %@ for managing and joining meetings." = "Hãy thử %@ để quản lý và tham gia cuộc họp.";
+"Meetings App" = "Ứng dụng Cuộc họp";
+
+/* MARK: - Đồng ý ghi âm */
+"Record this meeting locally?" = "Ghi âm cuộc họp này trên máy?";
+"With your consent, the app will record meeting audio on this Mac and show it in AI Companion after the meeting." = "Với sự đồng ý của bạn, ứng dụng sẽ ghi âm cuộc họp trên Mac này và hiển thị trong Trợ lý AI sau cuộc họp.";
+"Allow and Continue" = "Cho phép và tiếp tục";
+"Continue Without Recording" = "Tiếp tục không ghi âm";
+"Transcription Languages" = "Ngôn ngữ phiên âm";
+"Preferred Language 1" = "Ngôn ngữ ưu tiên 1";
+"Preferred Language 2" = "Ngôn ngữ ưu tiên 2";
+"Microphone permission denied" = "Quyền micro bị từ chối";
+"Microphone permission required" = "Cần quyền micro";
+"Enable microphone access in System Settings to record meetings locally." = "Bật quyền truy cập micro trong Cài đặt Hệ thống để ghi âm cuộc họp trên máy.";
+"Could not start recording" = "Không thể bắt đầu ghi âm";
+"Recording will start when the meeting starts" = "Ghi âm sẽ bắt đầu khi cuộc họp bắt đầu";
+"Meeting recording started (meeting + microphone)" = "Đã bắt đầu ghi âm cuộc họp (cuộc họp + micro)";
+"System audio unavailable. Recording microphone only." = "Âm thanh hệ thống không khả dụng. Chỉ ghi âm micro.";
+"Meeting recording started (microphone only)" = "Đã bắt đầu ghi âm cuộc họp (chỉ micro)";
+"Meeting recording saved" = "Đã lưu bản ghi âm cuộc họp";
+
+/* MARK: - Trợ lý AI */
+"Get notes from your previous meetings" = "Nhận ghi chú từ các cuộc họp trước";
+"Connect your Google account to see your AI Companion meeting history." = "Kết nối tài khoản Google để xem lịch sử cuộc họp Trợ lý AI.";
+"No previous meetings yet for %@. End a meeting to save it here and generate notes." = "Chưa có cuộc họp trước đó cho %@. Kết thúc cuộc họp để lưu tại đây và tạo ghi chú.";
+"this account" = "tài khoản này";
+"Previous meetings" = "Cuộc họp trước";
+"Recording in progress: %@" = "Đang ghi âm: %@";
+"Started: %@" = "Bắt đầu: %@";
+"Stop Recording" = "Dừng ghi âm";
+"Date: %@" = "Ngày: %@";
+"Saved time: %@" = "Đã lưu: %@";
+"Get Notes" = "Nhận ghi chú";
+"Premium required. Click to unlock AI Companion notes." = "Cần Premium. Nhấn để mở khóa ghi chú Trợ lý AI.";
+"Play Audio" = "Phát âm thanh";
+"Pause Audio" = "Tạm dừng âm thanh";
+"Stopped" = "Đã dừng";
+"Speaking..." = "Đang nói...";
+"Paused" = "Đã tạm dừng";
+"Playing..." = "Đang phát...";
+"Not playing" = "Không phát";
+"Checking audio..." = "Đang kiểm tra âm thanh...";
+"Audio file missing" = "Thiếu tệp âm thanh";
+"Could not play recording" = "Không thể phát bản ghi";
+"Failed: %@" = "Lỗi: %@";
+"Failed to play audio" = "Không thể phát âm thanh";
+"decode error" = "lỗi giải mã";
+"Loading..." = "Đang tải...";
+"Buffering..." = "Đang đệm...";
+"Finished" = "Hoàn tất";
+"Transcript not requested" = "Chưa yêu cầu bản ghi";
+"Transcript processing..." = "Đang xử lý bản ghi...";
+"Transcript ready" = "Bản ghi đã sẵn sàng";
+"Transcript unavailable (tap to retry)" = "Bản ghi không khả dụng (nhấn để thử lại)";
+"Notes not requested" = "Chưa yêu cầu ghi chú";
+"Preparing notes, please wait..." = "Đang chuẩn bị ghi chú, vui lòng đợi...";
+"Notes ready" = "Ghi chú đã sẵn sàng";
+"Notes unavailable (tap to retry)" = "Ghi chú không khả dụng (nhấn để thử lại)";
+"Transcript unavailable" = "Bản ghi không khả dụng";
+"Notes unavailable" = "Ghi chú không khả dụng";
+"Could not find recording details for this meeting." = "Không tìm thấy chi tiết ghi âm cho cuộc họp này.";
+"Transcript - %@" = "Bản ghi - %@";
+"Notes - %@" = "Ghi chú - %@";
+"Meeting Notes" = "Ghi chú cuộc họp";
+"Transcript processing..." = "Đang xử lý bản ghi...";
+
+/* MARK: - Màn hình thanh toán và StoreKit */
+"Meetings Pro" = "Cuộc họp Chuyên nghiệp";
+"PREMIUM EXPERIENCE" = "TRẢI NGHIỆM PREMIUM";
+"Elevate your meetings workflow" = "Nâng cao quy trình làm việc cuộc họp";
+"Unlock automation, premium meeting tools, and priority support for every session." = "Mở khóa tự động hóa, công cụ cuộc họp cao cấp và hỗ trợ ưu tiên cho mọi phiên.";
+"Manage meetings" = "Quản lý cuộc họp";
+"Smart scheduling" = "Lên lịch thông minh";
+"Faster workflow" = "Quy trình nhanh hơn";
+"Reminder notifications" = "Thông báo nhắc nhở";
+"Monthly" = "Hàng tháng";
+"Bi Yearly" = "6 tháng";
+"Yearly" = "Hàng năm";
+"Most Popular" = "Phổ biến nhất";
+"%@ • Most Popular" = "%@ • Phổ biến nhất";
+"3 days free trial" = "Dùng thử miễn phí 3 ngày";
+"Cancel anytime" = "Hủy bất cứ lúc nào";
+"No lock-in" = "Không ràng buộc";
+"Instant access" = "Truy cập ngay";
+"Unlock all tools" = "Mở khóa tất cả công cụ";
+"Secure billing" = "Thanh toán an toàn";
+"Handled by Apple" = "Do Apple xử lý";
+"Secured by Apple. Cancel anytime." = "Bảo mật bởi Apple. Hủy bất cứ lúc nào.";
+"Restore Purchase" = "Khôi phục mua hàng";
+"Continue with free plan" = "Tiếp tục với gói miễn phí";
+"Already Premium" = "Đã có Premium";
+"You are already premium. Do you want to continue with this purchase?" = "Bạn đã có Premium. Bạn có muốn tiếp tục mua hàng này không?";
+"Current plan: %@. Choose a different package to switch." = "Gói hiện tại: %@. Chọn gói khác để chuyển đổi.";
+"Premium is active on this Apple ID." = "Premium đang hoạt động trên Apple ID này.";
+"Free for %@, then %@" = "Miễn phí trong %@, sau đó %@";
+"Billed via App Store" = "Thanh toán qua App Store";
+"%@  •  %@" = "%@  •  %@";
+"Save 0%" = "Tiết kiệm 0%";
+"Save %@%%" = "Tiết kiệm %@%%";
+"month" = "tháng";
+"year" = "năm";
+"%@ free trial" = "Dùng thử miễn phí %@";
+"Processing..." = "Đang xử lý...";
+"Premium Active" = "Premium đang hoạt động";
+"Continue with 3 day trail" = "Tiếp tục dùng thử 3 ngày";
+"Please Wait" = "Vui lòng đợi";
+"A purchase is already being processed." = "Đang xử lý một giao dịch mua.";
+"This Apple ID already has premium access." = "Apple ID này đã có quyền truy cập Premium.";
+"Purchase Complete" = "Mua hàng hoàn tất";
+"Premium has been unlocked successfully." = "Đã mở khóa Premium thành công.";
+"Purchase Pending" = "Mua hàng đang chờ";
+"Your purchase is pending approval. You can continue once it completes." = "Giao dịch mua của bạn đang chờ phê duyệt. Bạn có thể tiếp tục sau khi hoàn tất.";
+"Product Not Available" = "Sản phẩm không khả dụng";
+"Unable to load this product. Check your StoreKit configuration and product IDs." = "Không thể tải sản phẩm này. Kiểm tra cấu hình StoreKit và ID sản phẩm.";
+"Already Purchased" = "Đã mua";
+"This plan is already active on your Apple ID." = "Gói này đã hoạt động trên Apple ID của bạn.";
+"Purchase Failed" = "Mua hàng thất bại";
+"Restore Purchases" = "Khôi phục mua hàng";
+"Purchases restored successfully." = "Đã khôi phục mua hàng thành công.";
+"No previous premium purchase was found for this Apple ID." = "Không tìm thấy giao dịch mua Premium trước đó cho Apple ID này.";
+"Restore failed: %@" = "Khôi phục thất bại: %@";
+"Unable to Open Subscriptions" = "Không thể mở Đăng ký";
+"The subscriptions URL is invalid." = "URL đăng ký không hợp lệ.";
+"%@ tapped." = "Đã nhấn %@.";
+
+/* MARK: - Xác thực */
+"Google Sign-In" = "Đăng nhập Google";
+
+/* MARK: - Lỗi nhận dạng giọng nói/phiên âm */
+"Speech recognition permission denied. Enable it in System Settings and try again." = "Quyền nhận dạng giọng nói bị từ chối. Bật trong Cài đặt Hệ thống và thử lại.";
+"Speech recognition is restricted on this Mac." = "Nhận dạng giọng nói bị hạn chế trên Mac này.";
+"Speech recognition authorization is unavailable." = "Ủy quyền nhận dạng giọng nói không khả dụng.";
+"Speech recognizer is unavailable for the current locale." = "Bộ nhận dạng giọng nói không khả dụng cho ngôn ngữ hiện tại.";
+"Speech recognizer is unavailable for %@." = "Bộ nhận dạng giọng nói không khả dụng cho %@.";
+"No audio was available to transcribe." = "Không có âm thanh để phiên âm.";
+
+/* MARK: - Lỗi giao dịch StoreKit */
+"Unable to load products: %@" = "Không thể tải sản phẩm: %@";
+"Product ID not found in StoreKit response. Requested: %@. Loaded IDs: %@" = "Không tìm thấy ID sản phẩm trong phản hồi StoreKit. Yêu cầu: %@. ID đã tải: %@";
+"Purchase verification failed." = "Xác minh mua hàng thất bại.";
+"Unknown purchase state." = "Trạng thái mua hàng không xác định.";
+"none" = "không có";
+
+/* MARK: - Văn bản giá màn hình thanh toán */
+"%@ one-time purchase" = "Mua một lần %@";
+"%@/month" = "%@/tháng";
+"%@/6 months" = "%@/6 tháng";
+"%@/year (3 days free trial)" = "%@/năm (dùng thử miễn phí 3 ngày)";
+"day" = "ngày";
+"week" = "tuần";
+"period" = "kỳ";
+"1 %@" = "1 %@";
+"%d %@s" = "%d %@";
+
+/* MARK: - Thông báo nhanh */
+"Meeting added successfully." = "Đã thêm cuộc họp thành công.";
+"An issue occurred. Please try again." = "Đã xảy ra sự cố. Vui lòng thử lại.";
+"Failed to load transcript." = "Không thể tải bản ghi.";
+"Failed to generate notes." = "Không thể tạo ghi chú.";
+
+/* MARK: - Cảnh báo trình duyệt */
+"Try Again" = "Thử lại";
+"The in-app browser closed this page unexpectedly. You can try loading it again in this same window." = "Trình duyệt trong ứng dụng đã đóng trang này bất ngờ. Bạn có thể thử tải lại trong cùng cửa sổ.";
+
+/* MARK: - Lỗi lên lịch và lịch */
+"Couldn't load schedule." = "Không thể tải lịch.";
+"Couldn't refresh schedule." = "Không thể làm mới lịch.";
+"Couldn't connect Google account." = "Không thể kết nối tài khoản Google.";
+"Couldn't logout Google account." = "Không thể đăng xuất tài khoản Google.";
+"Couldn't load schedule" = "Không thể tải lịch";
+"Couldn't determine Meet meeting code from URL." = "Không thể xác định mã cuộc họp Google Meet từ URL.";
+"Refreshing…" = "Đang làm mới…";
+"Connect Google to see meetings" = "Kết nối tài khoản Google để xem cuộc họp";
+"Connect to load schedule" = "Kết nối để tải lịch";
+"Loading…" = "Đang tải…";
+"Loading schedule…" = "Đang tải lịch…";
+"Start date must be on or before end date." = "Ngày bắt đầu phải trước hoặc bằng ngày kết thúc.";
+"Invalid custom date range" = "Khoảng ngày tùy chỉnh không hợp lệ";
+"No meetings" = "Không có cuộc họp";
+"No meetings for selected filters" = "Không có cuộc họp cho bộ lọc đã chọn";
+"No meetings on %@" = "Không có cuộc họp vào %@";
+"1 meeting on %@" = "1 cuộc họp vào %@";
+"%d meetings on %@" = "%d cuộc họp vào %@";
+"All day" = "Cả ngày";
+"Duration: all day" = "Thời lượng: cả ngày";
+"Duration: %dh %dm" = "Thời lượng: %dh %dm";
+"Duration: %dh" = "Thời lượng: %dh";
+"Duration: %dm" = "Thời lượng: %dm";
+"%@ - %@" = "%@ - %@";
+"Scheduled Meeting" = "Cuộc họp đã lên lịch";
+
+/* MARK: - Cửa sổ tạo cuộc họp */
+"Please enter a title." = "Vui lòng nhập tiêu đề.";
+"Duration must be a positive number of minutes." = "Thời lượng phải là số phút dương.";
+"Invalid start time." = "Thời gian bắt đầu không hợp lệ.";
+
+/* MARK: - Nhắc nhở và thông báo */
+"Reminders" = "Nhắc nhở";
+"1 Day Before" = "1 ngày trước";
+"12 Hours Before" = "12 giờ trước";
+"1 Hour Before" = "1 giờ trước";
+"Starts in 1 day" = "Bắt đầu sau 1 ngày";
+"Starts in 12 hours" = "Bắt đầu sau 12 giờ";
+"Starts in 1 hour" = "Bắt đầu sau 1 giờ";
+
+/* MARK: - Lỗi OAuth Google */
+"Missing Google OAuth Client ID." = "Thiếu ID ứng dụng khách Google OAuth.";
+"Missing Google OAuth Client Secret." = "Thiếu mã bí mật ứng dụng khách Google OAuth.";
+"Invalid OAuth callback URL." = "URL callback OAuth không hợp lệ.";
+"Google did not return an authorization code." = "Google không trả về mã ủy quyền.";
+"Token exchange failed: %@" = "Trao đổi token thất bại: %@";
+"Could not open browser for Google sign-in." = "Không thể mở trình duyệt để đăng nhập Google.";
+"Google sign-in timed out." = "Hết thời gian đăng nhập Google.";
+"No stored Google tokens found." = "Không tìm thấy token Google đã lưu.";
+
+/* MARK: - Lỗi Trợ lý AI/phiên âm */
+"Recording not found." = "Không tìm thấy bản ghi.";
+"Local meeting audio is missing." = "Thiếu âm thanh cuộc họp cục bộ.";
+"Generated transcript was empty." = "Bản ghi được tạo trống.";
+"(No transcript entries found.)" = "(Không tìm thấy mục bản ghi.)";
+"No conference record found for this meeting." = "Không tìm thấy bản ghi hội nghị cho cuộc họp này.";
+"No transcript found for this meeting." = "Không tìm thấy bản ghi cho cuộc họp này.";
+"Unable to create audio composition tracks." = "Không thể tạo track ghép âm thanh.";
+"Unable to create audio export session." = "Không thể tạo phiên xuất âm thanh.";
+"Audio mix export failed." = "Xuất hỗn hợp âm thanh thất bại.";
+"No display available for system audio capture." = "Không có màn hình khả dụng để thu âm thanh hệ thống.";
+"Cannot add audio writer input." = "Không thể thêm đầu vào ghi âm thanh.";
+
+/* MARK: - Lỗi API Google */
+"Google Meet returned an invalid response." = "Google Meet trả về phản hồi không hợp lệ.";
+"Google Meet API error (%d): %@" = "Lỗi API Google Meet (%d): %@";
+"Failed to parse Google Meet response: %@" = "Không thể phân tích phản hồi Google Meet: %@";
+"Google Calendar returned an invalid response." = "Google Calendar trả về phản hồi không hợp lệ.";
+"Google Calendar API error (%d): %@" = "Lỗi API Google Calendar (%d): %@";
+"Failed to parse Google Calendar events: %@" = "Không thể phân tích sự kiện Google Calendar: %@";
+
+/* MARK: - Lỗi OpenAI */
+"OpenAI API key is missing. Set OPENAI_API_KEY in the environment or app key vault." = "Thiếu khóa API OpenAI. Đặt OPENAI_API_KEY trong môi trường hoặc kho khóa ứng dụng.";
+"Notes generation returned an invalid response." = "Tạo ghi chú trả về phản hồi không hợp lệ.";
+"Notes generation failed (%d): %@" = "Tạo ghi chú thất bại (%d): %@";
+"Notes generation returned empty text." = "Tạo ghi chú trả về văn bản trống.";
+"OpenAI transcription returned an invalid response." = "Phiên âm OpenAI trả về phản hồi không hợp lệ.";
+"OpenAI transcription failed (%d): %@" = "Phiên âm OpenAI thất bại (%d): %@";
+"OpenAI transcription returned no text." = "Phiên âm OpenAI không trả về văn bản.";
+
+/* MARK: - Duyệt web và trình duyệt trong ứng dụng */
+"Open sites in the in-app browser (back, forward, reload, address bar). OAuth and “Continue in browser” flows stay inside the app." = "Mở trang web trong trình duyệt của ứng dụng (quay lại, tiến, tải lại, thanh địa chỉ). Luồng OAuth và «Tiếp tục trong trình duyệt» vẫn trong ứng dụng.";
+"Open in app browser" = "Mở trong trình duyệt ứng dụng";
+"Meet help" = "Trợ giúp Google Meet";
+"Go" = "Đi";
+"Back" = "Quay lại";
+"Forward" = "Tiến";
+"Reload" = "Tải lại";
+
+/* MARK: - Nhãn lịch và lên lịch */
+"Google Calendar" = "Google Calendar";
+"Untitled meeting" = "Cuộc họp không tiêu đề";
+"Created event missing start/end." = "Sự kiện đã tạo thiếu thời gian bắt đầu/kết thúc.";
+"Created event missing Meet/HTML link." = "Sự kiện đã tạo thiếu liên kết Google Meet/HTML.";
+
+/* MARK: - Tài khoản Google */
+"Google account" = "Tài khoản Google";
+"%@, Google account" = "%@, tài khoản Google";
+"Google User" = "Người dùng Google";
+
+/* MARK: - Tham gia Zoom (phần không dùng) */
+"Meeting ID" = "ID cuộc họp";
+"Enter meeting ID..." = "Nhập ID cuộc họp...";
+"Meeting Passcode" = "Mã truy cập cuộc họp";
+"Enter meeting passcode..." = "Nhập mã truy cập cuộc họp...";
+
+/* MARK: - Màn hình thanh toán (lợi ích thay thế) */
+"Virtual backgrounds" = "Phông nền ảo";
+"Tools for productivity" = "Công cụ năng suất";
+"24/7 support" = "Hỗ trợ 24/7";
+
+/* MARK: - Người nói trong bản ghi */
+"You" = "Bạn";
+
+/* MARK: - Lỗi Keychain */
+"Failed to read OAuth tokens from Keychain." = "Không thể đọc token OAuth từ Keychain.";
+"Keychain returned an invalid token payload." = "Keychain trả về dữ liệu token không hợp lệ.";
+"Failed to update OAuth tokens in Keychain." = "Không thể cập nhật token OAuth trong Keychain.";
+"Failed to save OAuth tokens to Keychain." = "Không thể lưu token OAuth vào Keychain.";
+"Failed to delete OAuth tokens from Keychain." = "Không thể xóa token OAuth khỏi Keychain.";
+"Unable to allocate audio buffer." = "Không thể cấp phát bộ đệm âm thanh.";
+"Invalid revoke response." = "Phản hồi thu hồi không hợp lệ.";
+"Token revoke failed with status %d." = "Thu hồi token thất bại với trạng thái %d.";
+
+/* MARK: - Chẩn đoán lỗi */
+"Keychain: %d (%@)" = "Keychain: %d (%@)";
+"OSStatus: %d" = "OSStatus: %d";
+"Item: service=%@, account=%@" = "Mục: service=%@, account=%@";

+ 17 - 0
meetings_app/vi.lproj/Main.strings

@@ -0,0 +1,17 @@
+/* Class = "NSMenuItem"; title = "AI Companion for Meet"; ObjectID = "1Xt-HY-uBw"; */
+"1Xt-HY-uBw.title" = "Trợ lý AI cho Google Meet";
+
+/* Class = "NSMenu"; title = "AI Companion for Meet"; ObjectID = "uQy-DD-JDr"; */
+"uQy-DD-JDr.title" = "Trợ lý AI cho Google Meet";
+
+/* Class = "NSMenuItem"; title = "About AI Companion for Meet"; ObjectID = "5kV-Vb-QxS"; */
+"5kV-Vb-QxS.title" = "Giới thiệu Trợ lý AI cho Google Meet";
+
+/* Class = "NSMenuItem"; title = "Hide AI Companion for Meet"; ObjectID = "Olw-nP-bQN"; */
+"Olw-nP-bQN.title" = "Ẩn Trợ lý AI cho Google Meet";
+
+/* Class = "NSMenuItem"; title = "Quit AI Companion for Meet"; ObjectID = "4sb-4s-VLi"; */
+"4sb-4s-VLi.title" = "Thoát Trợ lý AI cho Google Meet";
+
+/* Class = "NSMenuItem"; title = "AI Companion for Meet Help"; ObjectID = "FKE-Sm-Kum"; */
+"FKE-Sm-Kum.title" = "Trợ giúp Trợ lý AI cho Google Meet";