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