From 108b05c619b432f093e9cd64a15ce3bbc0e02840 Mon Sep 17 00:00:00 2001
From: Ariver <ar@MacBook-Air.local>
Date: Wed, 06 May 2026 18:25:40 +0800
Subject: [PATCH] checkpoint: v3.1.0 — 国际化:9语种 JSON 翻译文件 + Language 菜单 + L10n/tr()

---
 Apptag/ApptagApp.swift                  |   32 +++
 Apptag/Localization/en.json             |   46 +++++
 Apptag/Localization/zh-Hant.json        |   46 +++++
 Apptag/Localization/ja.json             |   46 +++++
 Apptag/Info.plist                       |    2 
 Apptag/Localization/fr.json             |   46 +++++
 build.sh                                |    3 
 CHANGELOG.md                            |    7 
 Apptag/Localization/ko.json             |   46 +++++
 Apptag/Localization/zh-Hans.json        |   46 +++++
 Apptag/Localization/it.json             |   46 +++++
 X-screencap/录制于 2026-05-06 18.05.17.mp4 |    0 
 Apptag/Localization/es.json             |   46 +++++
 Apptag/L10n.swift                       |   63 +++++++
 X-screencap/SCR-20260506-puyk.jpeg      |    0 
 Apptag/Localization/ru.json             |   46 +++++
 16 files changed, 515 insertions(+), 6 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index b93e706..640545d 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -24,7 +24,7 @@
     private var isInEditMode = false  // Suppress auto-dismiss during editing
 
     func applicationDidFinishLaunching(_ notification: Notification) {
-        // Use .accessory so the app appears in Force Quit (unlike LSUIElement)
+        L10n.setup()
         NSApp.setActivationPolicy(.accessory)
         setupMenuBar()
         registerHotkey()
@@ -50,7 +50,7 @@
         let menu = NSMenu()
         menu.addItem(
             NSMenuItem(
-                title: "Show Apptag",
+                title: tr("menu.show"),
                 action: #selector(toggleOverlay),
                 keyEquivalent: ""
             )
@@ -61,7 +61,7 @@
         let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?"
         let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
         let versionItem = NSMenuItem(
-            title: "Version \(appVersion) (\(buildNumber))",
+            title: "\(tr("menu.version")) \(appVersion) (\(buildNumber))",
             action: nil,
             keyEquivalent: ""
         )
@@ -70,16 +70,31 @@
 
         menu.addItem(.separator())
         let prefsItem = NSMenuItem(
-            title: "Preferences...",
+            title: tr("menu.preferences"),
             action: #selector(openPreferences),
             keyEquivalent: ","
         )
         prefsItem.keyEquivalentModifierMask = .command
         menu.addItem(prefsItem)
         menu.addItem(.separator())
+
+        // Language submenu
+        let langMenu = NSMenu()
+        let currentLang = UserDefaults.standard.string(forKey: "appLanguage") ?? "en"
+        for (code, name) in L10n.supported {
+            let item = NSMenuItem(title: name, action: #selector(switchLanguage(_:)), keyEquivalent: "")
+            item.representedObject = code
+            item.state = (code == currentLang) ? .on : .off
+            langMenu.addItem(item)
+        }
+        let langItem = NSMenuItem(title: tr("menu.language"), action: nil, keyEquivalent: "")
+        langItem.submenu = langMenu
+        menu.addItem(langItem)
+
+        menu.addItem(.separator())
         menu.addItem(
             NSMenuItem(
-                title: "Quit",
+                title: tr("menu.quit"),
                 action: #selector(NSApplication.terminate(_:)),
                 keyEquivalent: "q"
             )
@@ -237,6 +252,13 @@
         hideOverlay()
         NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
     }
+
+    @objc private func switchLanguage(_ sender: NSMenuItem) {
+        guard let code = sender.representedObject as? String else { return }
+        L10n.switchTo(code)
+        // Rebuild menu to update checkmarks
+        setupMenuBar()
+    }
 }
 
 // MARK: - NSView-level backdrop dismiss (works even if SwiftUI rendering is slow)
diff --git a/Apptag/Info.plist b/Apptag/Info.plist
index 05f1e46..4c4db55 100644
--- a/Apptag/Info.plist
+++ b/Apptag/Info.plist
@@ -19,7 +19,7 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>3.0.11</string>
+	<string>3.1.0</string>
 	<key>CFBundleVersion</key>
 	<string>1</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/Apptag/L10n.swift b/Apptag/L10n.swift
new file mode 100644
index 0000000..be24145
--- /dev/null
+++ b/Apptag/L10n.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+// MARK: - Localization Manager
+
+enum L10n {
+    static var current: [String: String] = [:]
+
+    /// Call once at startup to load the saved or system language.
+    static func setup() {
+        let code = UserDefaults.standard.string(forKey: "appLanguage") ?? fallbackCode()
+        load(code)
+    }
+
+    static let supported: [(code: String, name: String)] = [
+        ("en", "English"),
+        ("zh-Hans", "简体中文"),
+        ("zh-Hant", "繁體中文"),
+        ("ja", "日本語"),
+        ("ko", "한국어"),
+        ("ru", "Русский"),
+        ("fr", "Français"),
+        ("it", "Italiano"),
+        ("es", "Español"),
+    ]
+
+    static func switchTo(_ code: String) {
+        load(code)
+        UserDefaults.standard.set(code, forKey: "appLanguage")
+    }
+
+    private static func load(_ code: String) {
+        guard let url = Bundle.main.url(
+            forResource: code, withExtension: "json",
+            subdirectory: "Localization"
+        ) else {
+            // Fallback to bundled English
+            if code != "en" { load("en") }
+            return
+        }
+        guard let data = try? Data(contentsOf: url),
+              let dict = try? JSONSerialization.jsonObject(with: data) as? [String: String]
+        else { return }
+        current = dict
+    }
+
+    private static func fallbackCode() -> String {
+        let lang = Locale.preferredLanguages.first ?? "en"
+        if lang.hasPrefix("zh-Hant") || lang.hasPrefix("zh-HK") || lang.hasPrefix("zh-TW") { return "zh-Hant" }
+        if lang.hasPrefix("zh") { return "zh-Hans" }
+        if lang.hasPrefix("ja") { return "ja" }
+        if lang.hasPrefix("ko") { return "ko" }
+        if lang.hasPrefix("ru") { return "ru" }
+        if lang.hasPrefix("fr") { return "fr" }
+        if lang.hasPrefix("it") { return "it" }
+        if lang.hasPrefix("es") { return "es" }
+        return "en"
+    }
+}
+
+/// Convenience function — use `tr("key")` everywhere.
+func tr(_ key: String) -> String {
+    L10n.current[key] ?? key
+}
diff --git a/Apptag/Localization/en.json b/Apptag/Localization/en.json
new file mode 100644
index 0000000..93d5db5
--- /dev/null
+++ b/Apptag/Localization/en.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Show Apptag",
+  "menu.preferences": "Preferences…",
+  "menu.quit": "Quit",
+  "menu.language": "Language",
+  "menu.version": "Version",
+  "settings.general": "General",
+  "settings.tags": "Tags",
+  "settings.data": "Data",
+  "settings.about": "About",
+  "settings.appListStyle": "App list style:",
+  "settings.flat": "Flat",
+  "settings.container": "Container",
+  "settings.flatDesc": "\"Flat\" shows apps directly. \"Container\" wraps each tag group in a rounded box.",
+  "settings.hideAppNames": "Hide app names",
+  "settings.tagPosition": "Tag position:",
+  "settings.left": "Left",
+  "settings.right": "Right",
+  "settings.top": "Top",
+  "settings.tagPosDesc": "Where the tag navigation bar appears. Left/Right puts tags in a sidebar.",
+  "settings.tagFontSize": "Tag font size:",
+  "settings.tagFontDesc": "Adjust the size of group-name labels in the overlay.",
+  "settings.iconSize": "Icon size:",
+  "settings.iconSizeDesc": "Icon display size. Grid columns adjust automatically.",
+  "settings.reindex": "Re-index Now",
+  "settings.reindexDesc": "Re-scan all app directories and refresh the tag database.",
+  "settings.indexMgmt": "Index Management",
+  "settings.backup": "Backup & Restore",
+  "settings.export": "Export Tags…",
+  "settings.import": "Import Tags…",
+  "settings.backupDesc": "Export saves your tag assignments to a JSON file. Import replaces the current database with one from a file.",
+  "settings.importFailed": "Import Failed",
+  "edit.exit": "Exit editing",
+  "edit.title": "Edit App Categories",
+  "edit.confirm": "Confirm",
+  "edit.selectTags": "Select tags:",
+  "edit.dragHint": "Drag to reorder",
+  "edit.success": "分类成功",
+  "edit.noTags": "No tags yet. Click \"New Tag\" to create one.",
+  "tag.newTag": "New Tag",
+  "tag.name": "Tag name",
+  "tag.save": "Save",
+  "tag.cancel": "Cancel",
+  "tag.add": "Add",
+  "tag.newName": "New tag name"
+}
diff --git a/Apptag/Localization/es.json b/Apptag/Localization/es.json
new file mode 100644
index 0000000..ee82c08
--- /dev/null
+++ b/Apptag/Localization/es.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Mostrar Apptag",
+  "menu.preferences": "Preferencias…",
+  "menu.quit": "Salir",
+  "menu.language": "Idioma",
+  "menu.version": "Versión",
+  "settings.general": "General",
+  "settings.tags": "Etiquetas",
+  "settings.data": "Datos",
+  "settings.about": "Acerca de",
+  "settings.appListStyle": "Estilo de lista:",
+  "settings.flat": "Plano",
+  "settings.container": "Contenedor",
+  "settings.flatDesc": "«Plano» muestra apps directamente. «Contenedor» agrupa etiquetas en cajas.",
+  "settings.hideAppNames": "Ocultar nombres",
+  "settings.tagPosition": "Posición de etiquetas:",
+  "settings.left": "Izquierda",
+  "settings.right": "Derecha",
+  "settings.top": "Arriba",
+  "settings.tagPosDesc": "Dónde aparece la barra de navegación de etiquetas.",
+  "settings.tagFontSize": "Tamaño de fuente:",
+  "settings.tagFontDesc": "Ajusta el tamaño de las etiquetas de grupo.",
+  "settings.iconSize": "Tamaño de iconos:",
+  "settings.iconSizeDesc": "Tamaño de visualización de iconos.",
+  "settings.reindex": "Reindexar",
+  "settings.reindexDesc": "Volver a escanear todos los directorios de apps.",
+  "settings.indexMgmt": "Gestión de índice",
+  "settings.backup": "Copia de seguridad",
+  "settings.export": "Exportar etiquetas…",
+  "settings.import": "Importar etiquetas…",
+  "settings.backupDesc": "La exportación guarda las asignaciones de etiquetas en un archivo JSON.",
+  "settings.importFailed": "Error de importación",
+  "edit.exit": "Salir de edición",
+  "edit.title": "Editar categorías",
+  "edit.confirm": "Confirmar",
+  "edit.selectTags": "Seleccionar:",
+  "edit.dragHint": "Arrastrar para reordenar",
+  "edit.success": "Éxito",
+  "edit.noTags": "Sin etiquetas.",
+  "tag.newTag": "Nueva etiqueta",
+  "tag.name": "Nombre",
+  "tag.save": "Guardar",
+  "tag.cancel": "Cancelar",
+  "tag.add": "Añadir",
+  "tag.newName": "Nuevo nombre"
+}
\ No newline at end of file
diff --git a/Apptag/Localization/fr.json b/Apptag/Localization/fr.json
new file mode 100644
index 0000000..0fc1384
--- /dev/null
+++ b/Apptag/Localization/fr.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Afficher Apptag",
+  "menu.preferences": "Préférences…",
+  "menu.quit": "Quitter",
+  "menu.language": "Langue",
+  "menu.version": "Version",
+  "settings.general": "Général",
+  "settings.tags": "Étiquettes",
+  "settings.data": "Données",
+  "settings.about": "À propos",
+  "settings.appListStyle": "Style de liste:",
+  "settings.flat": "Plat",
+  "settings.container": "Conteneur",
+  "settings.flatDesc": "«Plat» affiche les apps directement. «Conteneur» groupe les étiquettes dans des boîtes.",
+  "settings.hideAppNames": "Masquer les noms",
+  "settings.tagPosition": "Position des étiquettes:",
+  "settings.left": "Gauche",
+  "settings.right": "Droite",
+  "settings.top": "Haut",
+  "settings.tagPosDesc": "Où la barre de navigation des étiquettes apparaît.",
+  "settings.tagFontSize": "Taille de police:",
+  "settings.tagFontDesc": "Ajuste la taille des noms de groupe dans l'overlay.",
+  "settings.iconSize": "Taille des icônes:",
+  "settings.iconSizeDesc": "Taille d'affichage des icônes.",
+  "settings.reindex": "Réindexer",
+  "settings.reindexDesc": "Réanalyser tous les répertoires d'applications.",
+  "settings.indexMgmt": "Gestion de l'index",
+  "settings.backup": "Sauvegarde et restauration",
+  "settings.export": "Exporter les étiquettes…",
+  "settings.import": "Importer les étiquettes…",
+  "settings.backupDesc": "L'exportation enregistre les attributions d'étiquettes dans un fichier JSON.",
+  "settings.importFailed": "Échec de l'importation",
+  "edit.exit": "Quitter l'édition",
+  "edit.title": "Modifier les catégories",
+  "edit.confirm": "Confirmer",
+  "edit.selectTags": "Sélectionner:",
+  "edit.dragHint": "Glisser pour réorganiser",
+  "edit.success": "Succès",
+  "edit.noTags": "Aucune étiquette.",
+  "tag.newTag": "Nouvelle étiquette",
+  "tag.name": "Nom",
+  "tag.save": "Enregistrer",
+  "tag.cancel": "Annuler",
+  "tag.add": "Ajouter",
+  "tag.newName": "Nouveau nom"
+}
\ No newline at end of file
diff --git a/Apptag/Localization/it.json b/Apptag/Localization/it.json
new file mode 100644
index 0000000..4002aba
--- /dev/null
+++ b/Apptag/Localization/it.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Mostra Apptag",
+  "menu.preferences": "Preferenze…",
+  "menu.quit": "Esci",
+  "menu.language": "Lingua",
+  "menu.version": "Versione",
+  "settings.general": "Generale",
+  "settings.tags": "Tag",
+  "settings.data": "Dati",
+  "settings.about": "Informazioni",
+  "settings.appListStyle": "Stile elenco:",
+  "settings.flat": "Piatto",
+  "settings.container": "Contenitore",
+  "settings.flatDesc": "«Piatto» mostra le app direttamente. «Contenitore» raggruppa i tag in riquadri.",
+  "settings.hideAppNames": "Nascondi nomi",
+  "settings.tagPosition": "Posizione tag:",
+  "settings.left": "Sinistra",
+  "settings.right": "Destra",
+  "settings.top": "Alto",
+  "settings.tagPosDesc": "Dove appare la barra di navigazione dei tag.",
+  "settings.tagFontSize": "Dimensione font:",
+  "settings.tagFontDesc": "Regola la dimensione delle etichette dei gruppi.",
+  "settings.iconSize": "Dimensione icone:",
+  "settings.iconSizeDesc": "Dimensione di visualizzazione delle icone.",
+  "settings.reindex": "Reindicizza",
+  "settings.reindexDesc": "Ripeti la scansione di tutte le directory delle app.",
+  "settings.indexMgmt": "Gestione indice",
+  "settings.backup": "Backup e ripristino",
+  "settings.export": "Esporta tag…",
+  "settings.import": "Importa tag…",
+  "settings.backupDesc": "L'esportazione salva le assegnazioni dei tag in un file JSON.",
+  "settings.importFailed": "Importazione fallita",
+  "edit.exit": "Esci dalla modifica",
+  "edit.title": "Modifica categorie",
+  "edit.confirm": "Conferma",
+  "edit.selectTags": "Seleziona tag:",
+  "edit.dragHint": "Trascina per riordinare",
+  "edit.success": "Successo",
+  "edit.noTags": "Nessun tag.",
+  "tag.newTag": "Nuovo tag",
+  "tag.name": "Nome tag",
+  "tag.save": "Salva",
+  "tag.cancel": "Annulla",
+  "tag.add": "Aggiungi",
+  "tag.newName": "Nuovo nome"
+}
\ No newline at end of file
diff --git a/Apptag/Localization/ja.json b/Apptag/Localization/ja.json
new file mode 100644
index 0000000..fe04480
--- /dev/null
+++ b/Apptag/Localization/ja.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Apptagを表示",
+  "menu.preferences": "設定…",
+  "menu.quit": "終了",
+  "menu.language": "言語",
+  "menu.version": "バージョン",
+  "settings.general": "一般",
+  "settings.tags": "タグ",
+  "settings.data": "データ",
+  "settings.about": "について",
+  "settings.appListStyle": "Appリストスタイル:",
+  "settings.flat": "フラット",
+  "settings.container": "コンテナ",
+  "settings.flatDesc": "「フラット」はAppを直接表示。「コンテナ」は各タググループを角丸ボックスで囲みます。",
+  "settings.hideAppNames": "App名を隠す",
+  "settings.tagPosition": "タグナビ位置:",
+  "settings.left": "左",
+  "settings.right": "右",
+  "settings.top": "上",
+  "settings.tagPosDesc": "タグナビゲーションバーの表示位置。左/右はサイドバーに表示します。",
+  "settings.tagFontSize": "タグフォントサイズ:",
+  "settings.tagFontDesc": "オーバーレイ内のグループ名ラベルのサイズを調整します。",
+  "settings.iconSize": "アイコンサイズ:",
+  "settings.iconSizeDesc": "アイコンの表示サイズ。グリッド列は自動調整されます。",
+  "settings.reindex": "再インデックス",
+  "settings.reindexDesc": "すべてのAppディレクトリを再スキャンし、タグデータベースを更新します。",
+  "settings.indexMgmt": "インデックス管理",
+  "settings.backup": "バックアップと復元",
+  "settings.export": "タグをエクスポート…",
+  "settings.import": "タグをインポート…",
+  "settings.backupDesc": "エクスポートはタグ割り当てをJSONファイルに保存します。インポートはファイルのデータで現在のデータベースを置き換えます。",
+  "settings.importFailed": "インポート失敗",
+  "edit.exit": "編集を終了",
+  "edit.title": "App分類を編集",
+  "edit.confirm": "確認",
+  "edit.selectTags": "タグを選択:",
+  "edit.dragHint": "ドラッグで並び替え",
+  "edit.success": "分類成功",
+  "edit.noTags": "タグがありません。「新規タグ」をクリックして作成してください。",
+  "tag.newTag": "新規タグ",
+  "tag.name": "タグ名",
+  "tag.save": "保存",
+  "tag.cancel": "キャンセル",
+  "tag.add": "追加",
+  "tag.newName": "新規タグ名"
+}
diff --git a/Apptag/Localization/ko.json b/Apptag/Localization/ko.json
new file mode 100644
index 0000000..cee9fdd
--- /dev/null
+++ b/Apptag/Localization/ko.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Apptag 보기",
+  "menu.preferences": "설정…",
+  "menu.quit": "종료",
+  "menu.language": "언어",
+  "menu.version": "버전",
+  "settings.general": "일반",
+  "settings.tags": "태그",
+  "settings.data": "데이터",
+  "settings.about": "정보",
+  "settings.appListStyle": "앱 목록 스타일:",
+  "settings.flat": "플랫",
+  "settings.container": "컨테이너",
+  "settings.flatDesc": "「플랫」은 앱을 직접 표시합니다.「컨테이너」는 각 태그 그룹을 둥근 상자로 묶습니다.",
+  "settings.hideAppNames": "앱 이름 숨기기",
+  "settings.tagPosition": "태그 탐색 위치:",
+  "settings.left": "왼쪽",
+  "settings.right": "오른쪽",
+  "settings.top": "위쪽",
+  "settings.tagPosDesc": "태그 탐색 막대의 표시 위치입니다.",
+  "settings.tagFontSize": "태그 글꼴 크기:",
+  "settings.tagFontDesc": "오버레이에서 그룹 이름 레이블의 크기를 조정합니다.",
+  "settings.iconSize": "아이콘 크기:",
+  "settings.iconSizeDesc": "아이콘 표시 크기입니다.",
+  "settings.reindex": "재인덱싱",
+  "settings.reindexDesc": "모든 앱 디렉토리를 다시 스캔하고 태그 데이터베이스를 새로 고칩니다.",
+  "settings.indexMgmt": "인덱스 관리",
+  "settings.backup": "백업 및 복원",
+  "settings.export": "태그 내보내기…",
+  "settings.import": "태그 가져오기…",
+  "settings.backupDesc": "내보내기는 태그 할당을 JSON 파일로 저장합니다.",
+  "settings.importFailed": "가져오기 실패",
+  "edit.exit": "편집 종료",
+  "edit.title": "앱 분류 편집",
+  "edit.confirm": "확인",
+  "edit.selectTags": "태그 선택:",
+  "edit.dragHint": "드래그하여 재정렬",
+  "edit.success": "분류 성공",
+  "edit.noTags": "태그가 없습니다.",
+  "tag.newTag": "새 태그",
+  "tag.name": "태그 이름",
+  "tag.save": "저장",
+  "tag.cancel": "취소",
+  "tag.add": "추가",
+  "tag.newName": "새 태그 이름"
+}
\ No newline at end of file
diff --git a/Apptag/Localization/ru.json b/Apptag/Localization/ru.json
new file mode 100644
index 0000000..fac1abc
--- /dev/null
+++ b/Apptag/Localization/ru.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "Показать Apptag",
+  "menu.preferences": "Настройки…",
+  "menu.quit": "Выход",
+  "menu.language": "Язык",
+  "menu.version": "Версия",
+  "settings.general": "Общие",
+  "settings.tags": "Теги",
+  "settings.data": "Данные",
+  "settings.about": "О программе",
+  "settings.appListStyle": "Стиль списка:",
+  "settings.flat": "Плитка",
+  "settings.container": "Контейнер",
+  "settings.flatDesc": "«Плитка» показывает приложения напрямую. «Контейнер» помещает группы в рамки.",
+  "settings.hideAppNames": "Скрыть названия",
+  "settings.tagPosition": "Позиция тегов:",
+  "settings.left": "Слева",
+  "settings.right": "Справа",
+  "settings.top": "Сверху",
+  "settings.tagPosDesc": "Где отображается панель навигации по тегам.",
+  "settings.tagFontSize": "Размер шрифта:",
+  "settings.tagFontDesc": "Размер названий групп в оверлее.",
+  "settings.iconSize": "Размер иконок:",
+  "settings.iconSizeDesc": "Размер отображения иконок.",
+  "settings.reindex": "Переиндексировать",
+  "settings.reindexDesc": "Пересканировать все директории приложений.",
+  "settings.indexMgmt": "Управление индексом",
+  "settings.backup": "Резервное копирование",
+  "settings.export": "Экспорт тегов…",
+  "settings.import": "Импорт тегов…",
+  "settings.backupDesc": "Экспорт сохраняет назначения тегов в JSON-файл.",
+  "settings.importFailed": "Ошибка импорта",
+  "edit.exit": "Выйти из редактирования",
+  "edit.title": "Редактировать категории",
+  "edit.confirm": "Подтвердить",
+  "edit.selectTags": "Выбрать теги:",
+  "edit.dragHint": "Перетащите для сортировки",
+  "edit.success": "Готово",
+  "edit.noTags": "Нет тегов.",
+  "tag.newTag": "Новый тег",
+  "tag.name": "Имя тега",
+  "tag.save": "Сохранить",
+  "tag.cancel": "Отмена",
+  "tag.add": "Добавить",
+  "tag.newName": "Новое имя тега"
+}
\ No newline at end of file
diff --git a/Apptag/Localization/zh-Hans.json b/Apptag/Localization/zh-Hans.json
new file mode 100644
index 0000000..4ccead6
--- /dev/null
+++ b/Apptag/Localization/zh-Hans.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "显示 Apptag",
+  "menu.preferences": "设置…",
+  "menu.quit": "退出",
+  "menu.language": "语言",
+  "menu.version": "版本",
+  "settings.general": "通用",
+  "settings.tags": "标签",
+  "settings.data": "数据",
+  "settings.about": "关于",
+  "settings.appListStyle": "App 列表样式:",
+  "settings.flat": "平铺",
+  "settings.container": "容器",
+  "settings.flatDesc": "「平铺」直接显示 App。「容器」将每个标签组放入圆角框。",
+  "settings.hideAppNames": "隐藏 App 名称",
+  "settings.tagPosition": "标签导航位置:",
+  "settings.left": "左侧",
+  "settings.right": "右侧",
+  "settings.top": "顶部",
+  "settings.tagPosDesc": "标签导航栏的显示位置。左/右将在侧边栏中显示标签。",
+  "settings.tagFontSize": "标签字体大小:",
+  "settings.tagFontDesc": "调整浮层中分组名称标签的大小。",
+  "settings.iconSize": "图标大小:",
+  "settings.iconSizeDesc": "图标显示尺寸。网格列会自动调整。",
+  "settings.reindex": "重新索引",
+  "settings.reindexDesc": "重新扫描所有 App 目录并刷新标签数据库。",
+  "settings.indexMgmt": "索引管理",
+  "settings.backup": "备份与恢复",
+  "settings.export": "导出标签…",
+  "settings.import": "导入标签…",
+  "settings.backupDesc": "导出将标签分配保存为 JSON 文件。导入将用文件中的数据替换当前数据库。",
+  "settings.importFailed": "导入失败",
+  "edit.exit": "退出编辑",
+  "edit.title": "编辑 App 分类",
+  "edit.confirm": "确认",
+  "edit.selectTags": "选择标签:",
+  "edit.dragHint": "拖动排序",
+  "edit.success": "分类成功",
+  "edit.noTags": "暂无标签。点击「新建标签」创建一个。",
+  "tag.newTag": "新建标签",
+  "tag.name": "标签名称",
+  "tag.save": "保存",
+  "tag.cancel": "取消",
+  "tag.add": "添加",
+  "tag.newName": "新标签名称"
+}
diff --git a/Apptag/Localization/zh-Hant.json b/Apptag/Localization/zh-Hant.json
new file mode 100644
index 0000000..8f9cd8c
--- /dev/null
+++ b/Apptag/Localization/zh-Hant.json
@@ -0,0 +1,46 @@
+{
+  "menu.show": "顯示 Apptag",
+  "menu.preferences": "設定…",
+  "menu.quit": "退出",
+  "menu.language": "語言",
+  "menu.version": "版本",
+  "settings.general": "一般",
+  "settings.tags": "標籤",
+  "settings.data": "資料",
+  "settings.about": "關於",
+  "settings.appListStyle": "App 列表樣式:",
+  "settings.flat": "平鋪",
+  "settings.container": "容器",
+  "settings.flatDesc": "「平鋪」直接顯示 App。「容器」將每個標籤組放入圓角框。",
+  "settings.hideAppNames": "隱藏 App 名稱",
+  "settings.tagPosition": "標籤導航位置:",
+  "settings.left": "左側",
+  "settings.right": "右側",
+  "settings.top": "頂部",
+  "settings.tagPosDesc": "標籤導航欄的顯示位置。左/右將在側邊欄中顯示標籤。",
+  "settings.tagFontSize": "標籤字型大小:",
+  "settings.tagFontDesc": "調整浮層中分組名稱標籤的大小。",
+  "settings.iconSize": "圖示大小:",
+  "settings.iconSizeDesc": "圖示顯示尺寸。網格列會自動調整。",
+  "settings.reindex": "重新索引",
+  "settings.reindexDesc": "重新掃描所有 App 目錄並重新整理標籤資料庫。",
+  "settings.indexMgmt": "索引管理",
+  "settings.backup": "備份與還原",
+  "settings.export": "匯出標籤…",
+  "settings.import": "匯入標籤…",
+  "settings.backupDesc": "匯出將標籤指派儲存為 JSON 檔案。匯入將用檔案中的資料取代目前資料庫。",
+  "settings.importFailed": "匯入失敗",
+  "edit.exit": "退出編輯",
+  "edit.title": "編輯 App 分類",
+  "edit.confirm": "確認",
+  "edit.selectTags": "選擇標籤:",
+  "edit.dragHint": "拖曳排序",
+  "edit.success": "分類成功",
+  "edit.noTags": "尚無標籤。點擊「新增標籤」建立一個。",
+  "tag.newTag": "新增標籤",
+  "tag.name": "標籤名稱",
+  "tag.save": "儲存",
+  "tag.cancel": "取消",
+  "tag.add": "新增",
+  "tag.newName": "新標籤名稱"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3cbd1a..9beb6ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Apptag Changelog
 
+## [3.1.0] — 2026-05-06
+
+- 国际化:支持 9 种语言(英/简中/繁中/日/韩/俄/法/意/西)
+- 菜单栏新增 Language 子菜单切换语言
+- 翻译文件独立存储在 `Apptag/Localization/*.json`
+- `L10n.swift` 管理器 + `tr()` 辅助函数
+
 ## [3.0.11] — 2026-05-06
 
 - 容器标题最小宽度优化:`.layoutPriority(1)` 让文字优先占空间,分隔线压缩,可显示约15字符
diff --git a/X-screencap/SCR-20260506-puyk.jpeg b/X-screencap/SCR-20260506-puyk.jpeg
new file mode 100644
index 0000000..e940639
--- /dev/null
+++ b/X-screencap/SCR-20260506-puyk.jpeg
Binary files differ
diff --git "a/X-screencap/\345\275\225\345\210\266\344\272\216 2026-05-06 18.05.17.mp4" "b/X-screencap/\345\275\225\345\210\266\344\272\216 2026-05-06 18.05.17.mp4"
new file mode 100644
index 0000000..7ae6362
--- /dev/null
+++ "b/X-screencap/\345\275\225\345\210\266\344\272\216 2026-05-06 18.05.17.mp4"
Binary files differ
diff --git a/build.sh b/build.sh
index ab05c50..970a371 100644
--- a/build.sh
+++ b/build.sh
@@ -39,6 +39,9 @@
     iconutil -c icns "$SWIFT_DIR/AppIcon.iconset" -o "$RESOURCES_DIR/AppIcon.icns" 2>/dev/null
 fi
 
+echo "==> Copying Localization files..."
+cp -r "$SWIFT_DIR/Localization" "$RESOURCES_DIR/Localization"
+
 echo "==> Setting permissions..."
 chmod +x "$MACOS_DIR/$APP_NAME"
 

--
Gitblit v1.9.3