···11+export { i18n } from './translationService';
22+export { languageNames } from './locales';
+86
src/i18n/locales/ar.json
···11+{
22+ "settings": {
33+ "title": "إعدادات AI Tagger",
44+ "provider": {
55+ "title": "مزود الذكاء الاصطناعي",
66+ "desc": "اختر مزود الذكاء الاصطناعي لإنشاء العلامات.",
77+ "anthropic": "Anthropic (Claude)",
88+ "openai": "OpenAI (GPT)",
99+ "mistral": "Mistral AI",
1010+ "google": "Google (Gemini)",
1111+ "custom": "نقطة نهاية مخصصة (متوافقة مع OpenAI)"
1212+ },
1313+ "customEndpoint": {
1414+ "title": "نقطة نهاية API مخصصة",
1515+ "desc": "أدخل عنوان URL لنقطة نهاية API المتوافقة مع OpenAI.",
1616+ "placeholder": "https://your-api-endpoint.com/v1/chat/completions"
1717+ },
1818+ "apiKey": {
1919+ "title": "مفتاح API",
2020+ "desc": "مفتاح API الخاص بـ {provider}. مطلوب لاستخدام خدمة الذكاء الاصطناعي.",
2121+ "getKey": "احصل عليه من {url} إذا لم يكن لديك واحد بالفعل.",
2222+ "recommendation": "نوصي باستخدام مفتاح مخصص لهذا البرنامج المساعد.",
2323+ "placeholder": "أدخل مفتاح API الخاص بك"
2424+ },
2525+ "model": {
2626+ "title": "نموذج الذكاء الاصطناعي",
2727+ "desc": "اختر نموذج الذكاء الاصطناعي المستخدم لإنشاء العلامات.",
2828+ "custom": "{model} (مخصص)"
2929+ },
3030+ "maxTags": {
3131+ "title": "الحد الأقصى لعدد العلامات",
3232+ "desc": "حدد الحد الأقصى لعدد العلامات لإنشائها لكل ملاحظة."
3333+ },
3434+ "promptStyle": {
3535+ "title": "نمط المطالبة",
3636+ "desc": "اختر نمط مطالبة محدد مسبقًا أو قم بإنشاء مطالبة مخصصة خاصة بك.",
3737+ "standard": "قياسي",
3838+ "descriptive": "وصفي",
3939+ "academic": "أكاديمي",
4040+ "concise": "موجز",
4141+ "custom": "مخصص"
4242+ },
4343+ "customPrompt": {
4444+ "title": "قالب مطالبة مخصص",
4545+ "desc": "قم بتخصيص المطالبة المرسلة إلى الذكاء الاصطناعي. استخدم {maxTags} و {content} كعناصر نائبة."
4646+ },
4747+ "currentPrompt": {
4848+ "title": "قالب المطالبة الحالي",
4949+ "desc": "هذا هو قالب المطالبة الذي سيتم استخدامه (للقراءة فقط). قم بالتبديل إلى مخصص إذا كنت ترغب في تعديله."
5050+ },
5151+ "language": {
5252+ "title": "اللغة",
5353+ "desc": "اختر لغة واجهة البرنامج المساعد."
5454+ }
5555+ },
5656+ "modals": {
5757+ "confirm": "تأكيد",
5858+ "cancel": "إلغاء",
5959+ "settings": "فتح الإعدادات",
6060+ "configError": "خطأ في تكوين علامات الذكاء الاصطناعي",
6161+ "tagAll": "سيقوم هذا بوضع علامات على جميع الملاحظات في خزانتك باستخدام نموذج {model}. قد يستغرق هذا بعض الوقت ويستهلك رصيد API. هل تريد المتابعة؟"
6262+ },
6363+ "notices": {
6464+ "noActiveNote": "لا توجد ملاحظة نشطة للتعليم",
6565+ "analyzing": "تحليل محتوى الملاحظة وإنشاء العلامات...",
6666+ "tagSuccess": "تمت إضافة العلامات بنجاح: {tags}",
6767+ "tagError": "خطأ في وضع علامة على الملاحظة: {error}",
6868+ "startingBulk": "بدء وضع علامات على الملاحظات...",
6969+ "processingNote": "معالجة: {file}\nالتقدم: {processed}/{total} ({successful} ناجح)",
7070+ "bulkComplete": "اكتمل وضع العلامات: {successful}/{total} ملاحظات بنجاح",
7171+ "bulkError": "خطأ أثناء وضع العلامات بالجملة: {error}"
7272+ },
7373+ "errors": {
7474+ "apiKeyMissing": "مفتاح API غير مكوّن. يرجى إضافة مفتاح API الخاص بك في إعدادات البرنامج المساعد.",
7575+ "endpointMissing": "نقطة نهاية API المخصصة غير مكوّنة. يرجى إضافة عنوان URL الخاص بنقطة النهاية في إعدادات البرنامج المساعد.",
7676+ "apiError": "فشل في إنشاء العلامات: {error}",
7777+ "unknownProvider": "مزود غير معروف: {provider}"
7878+ },
7979+ "commands": {
8080+ "tagCurrent": "وضع علامة على الملاحظة الحالية باستخدام الذكاء الاصطناعي",
8181+ "tagAll": "وضع علامات على جميع الملاحظات باستخدام الذكاء الاصطناعي"
8282+ },
8383+ "ribbon": {
8484+ "tooltip": "وضع علامات تلقائية باستخدام الذكاء الاصطناعي"
8585+ }
8686+}
+86
src/i18n/locales/de.json
···11+{
22+ "settings": {
33+ "title": "AI Tagger Einstellungen",
44+ "provider": {
55+ "title": "KI-Anbieter",
66+ "desc": "Wählen Sie, welchen KI-Anbieter Sie für die Generierung von Tags verwenden möchten.",
77+ "anthropic": "Anthropic (Claude)",
88+ "openai": "OpenAI (GPT)",
99+ "mistral": "Mistral AI",
1010+ "google": "Google (Gemini)",
1111+ "custom": "Benutzerdefinierter Endpunkt (OpenAI-kompatibel)"
1212+ },
1313+ "customEndpoint": {
1414+ "title": "Benutzerdefinierter API-Endpunkt",
1515+ "desc": "Geben Sie die URL für Ihren benutzerdefinierten OpenAI-kompatiblen API-Endpunkt ein.",
1616+ "placeholder": "https://your-api-endpoint.com/v1/chat/completions"
1717+ },
1818+ "apiKey": {
1919+ "title": "API-Schlüssel",
2020+ "desc": "Ihr {provider} API-Schlüssel. Erforderlich, um den KI-Dienst zu nutzen.",
2121+ "getKey": "Holen Sie ihn von {url}, wenn Sie noch keinen haben.",
2222+ "recommendation": "Wir empfehlen, einen dedizierten Schlüssel für dieses Plugin zu verwenden.",
2323+ "placeholder": "Geben Sie Ihren API-Schlüssel ein"
2424+ },
2525+ "model": {
2626+ "title": "KI-Modell",
2727+ "desc": "Wählen Sie, welches KI-Modell für die Tag-Generierung verwendet werden soll.",
2828+ "custom": "{model} (Benutzerdefiniert)"
2929+ },
3030+ "maxTags": {
3131+ "title": "Maximale Anzahl an Tags",
3232+ "desc": "Legen Sie die maximale Anzahl an Tags fest, die pro Notiz generiert werden sollen."
3333+ },
3434+ "promptStyle": {
3535+ "title": "Prompt-Stil",
3636+ "desc": "Wählen Sie einen vordefinierten Prompt-Stil oder erstellen Sie Ihren eigenen benutzerdefinierten Prompt.",
3737+ "standard": "Standard",
3838+ "descriptive": "Beschreibend",
3939+ "academic": "Akademisch",
4040+ "concise": "Prägnant",
4141+ "custom": "Benutzerdefiniert"
4242+ },
4343+ "customPrompt": {
4444+ "title": "Benutzerdefinierte Prompt-Vorlage",
4545+ "desc": "Passen Sie den an die KI gesendeten Prompt an. Verwenden Sie {maxTags} und {content} als Platzhalter."
4646+ },
4747+ "currentPrompt": {
4848+ "title": "Aktuelle Prompt-Vorlage",
4949+ "desc": "Dies ist die Prompt-Vorlage, die verwendet wird (schreibgeschützt). Wechseln Sie zu Benutzerdefiniert, wenn Sie sie bearbeiten möchten."
5050+ },
5151+ "language": {
5252+ "title": "Sprache",
5353+ "desc": "Wählen Sie die Sprache für die Plugin-Oberfläche."
5454+ }
5555+ },
5656+ "modals": {
5757+ "confirm": "Bestätigen",
5858+ "cancel": "Abbrechen",
5959+ "settings": "Einstellungen öffnen",
6060+ "configError": "KI-Tagging Konfigurationsfehler",
6161+ "tagAll": "Dies wird alle Notizen in Ihrem Vault mit dem Modell {model} taggen. Dies kann einige Zeit dauern und API-Guthaben verbrauchen. Möchten Sie fortfahren?"
6262+ },
6363+ "notices": {
6464+ "noActiveNote": "Keine aktive Notiz zum Taggen",
6565+ "analyzing": "Analysiere Notizinhalt und generiere Tags...",
6666+ "tagSuccess": "Tags erfolgreich hinzugefügt: {tags}",
6767+ "tagError": "Fehler beim Taggen der Notiz: {error}",
6868+ "startingBulk": "Beginne mit dem Taggen von Notizen...",
6969+ "processingNote": "Verarbeite: {file}\nFortschritt: {processed}/{total} ({successful} erfolgreich)",
7070+ "bulkComplete": "Tagging von {successful}/{total} Notizen erfolgreich abgeschlossen",
7171+ "bulkError": "Fehler beim Massen-Tagging: {error}"
7272+ },
7373+ "errors": {
7474+ "apiKeyMissing": "API-Schlüssel nicht konfiguriert. Bitte fügen Sie Ihren API-Schlüssel in den Plugin-Einstellungen hinzu.",
7575+ "endpointMissing": "Benutzerdefinierter API-Endpunkt nicht konfiguriert. Bitte fügen Sie Ihre Endpunkt-URL in den Plugin-Einstellungen hinzu.",
7676+ "apiError": "Fehler beim Generieren von Tags: {error}",
7777+ "unknownProvider": "Unbekannter Anbieter: {provider}"
7878+ },
7979+ "commands": {
8080+ "tagCurrent": "Aktuelle Notiz mit KI taggen",
8181+ "tagAll": "Alle Notizen mit KI taggen"
8282+ },
8383+ "ribbon": {
8484+ "tooltip": "Auto-Tagging mit KI"
8585+ }
8686+}
+86
src/i18n/locales/en.json
···11+{
22+ "settings": {
33+ "title": "AI Tagger Settings",
44+ "provider": {
55+ "title": "AI Provider",
66+ "desc": "Select which AI provider to use for generating tags.",
77+ "anthropic": "Anthropic (Claude)",
88+ "openai": "OpenAI (GPT)",
99+ "mistral": "Mistral AI",
1010+ "google": "Google (Gemini)",
1111+ "custom": "Custom Endpoint (OpenAI Compatible)"
1212+ },
1313+ "customEndpoint": {
1414+ "title": "Custom API Endpoint",
1515+ "desc": "Enter the URL for your custom OpenAI-compatible API endpoint.",
1616+ "placeholder": "https://your-api-endpoint.com/v1/chat/completions"
1717+ },
1818+ "apiKey": {
1919+ "title": "API Key",
2020+ "desc": "Your {provider} API key. Required to use the AI service.",
2121+ "getKey": "Get it from {url} if you don't have one already.",
2222+ "recommendation": "We recommend using a dedicated key for this plugin.",
2323+ "placeholder": "Enter your API key"
2424+ },
2525+ "model": {
2626+ "title": "AI Model",
2727+ "desc": "Choose which AI model to use for tag generation.",
2828+ "custom": "{model} (Custom)"
2929+ },
3030+ "maxTags": {
3131+ "title": "Maximum number of tags",
3232+ "desc": "Set the maximum number of tags to generate per note."
3333+ },
3434+ "promptStyle": {
3535+ "title": "Prompt Style",
3636+ "desc": "Choose a predefined prompt style or create your own custom prompt.",
3737+ "standard": "Standard",
3838+ "descriptive": "Descriptive",
3939+ "academic": "Academic",
4040+ "concise": "Concise",
4141+ "custom": "Custom"
4242+ },
4343+ "customPrompt": {
4444+ "title": "Custom prompt template",
4545+ "desc": "Customize the prompt sent to the AI. Use {maxTags} and {content} as placeholders."
4646+ },
4747+ "currentPrompt": {
4848+ "title": "Current prompt template",
4949+ "desc": "This is the prompt template that will be used (read-only). Switch to Custom if you want to edit it."
5050+ },
5151+ "language": {
5252+ "title": "Language",
5353+ "desc": "Choose the language for the plugin interface."
5454+ }
5555+ },
5656+ "modals": {
5757+ "confirm": "Confirm",
5858+ "cancel": "Cancel",
5959+ "settings": "Open settings",
6060+ "configError": "AI tagging configuration error",
6161+ "tagAll": "This will tag all notes in your vault using the {model} model. This may take a while and consume API credits. Do you want to continue?"
6262+ },
6363+ "notices": {
6464+ "noActiveNote": "No active note to tag",
6565+ "analyzing": "Analyzing note content and generating tags...",
6666+ "tagSuccess": "Successfully added tags: {tags}",
6767+ "tagError": "Error tagging note: {error}",
6868+ "startingBulk": "Starting to tag notes...",
6969+ "processingNote": "Processing: {file}\nProgress: {processed}/{total} ({successful} successful)",
7070+ "bulkComplete": "Completed tagging {successful}/{total} notes successfully",
7171+ "bulkError": "Error during bulk tagging: {error}"
7272+ },
7373+ "errors": {
7474+ "apiKeyMissing": "API key not configured. Please add your API key in the plugin settings.",
7575+ "endpointMissing": "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings.",
7676+ "apiError": "Failed to generate tags: {error}",
7777+ "unknownProvider": "Unknown provider: {provider}"
7878+ },
7979+ "commands": {
8080+ "tagCurrent": "Tag current note with AI",
8181+ "tagAll": "Tag all notes with AI"
8282+ },
8383+ "ribbon": {
8484+ "tooltip": "Auto-tag with AI"
8585+ }
8686+}
+86
src/i18n/locales/es.json
···11+{
22+ "settings": {
33+ "title": "Configuración de AI Tagger",
44+ "provider": {
55+ "title": "Proveedor de IA",
66+ "desc": "Seleccione qué proveedor de IA usar para generar etiquetas.",
77+ "anthropic": "Anthropic (Claude)",
88+ "openai": "OpenAI (GPT)",
99+ "mistral": "Mistral AI",
1010+ "google": "Google (Gemini)",
1111+ "custom": "Endpoint personalizado (Compatible con OpenAI)"
1212+ },
1313+ "customEndpoint": {
1414+ "title": "Endpoint API Personalizado",
1515+ "desc": "Ingrese la URL para su endpoint API compatible con OpenAI.",
1616+ "placeholder": "https://your-api-endpoint.com/v1/chat/completions"
1717+ },
1818+ "apiKey": {
1919+ "title": "Clave API",
2020+ "desc": "Su clave API de {provider}. Necesaria para usar el servicio de IA.",
2121+ "getKey": "Obténgala desde {url} si aún no tiene una.",
2222+ "recommendation": "Recomendamos usar una clave dedicada para este plugin.",
2323+ "placeholder": "Ingrese su clave API"
2424+ },
2525+ "model": {
2626+ "title": "Modelo de IA",
2727+ "desc": "Elija qué modelo de IA usar para generar etiquetas.",
2828+ "custom": "{model} (Personalizado)"
2929+ },
3030+ "maxTags": {
3131+ "title": "Número máximo de etiquetas",
3232+ "desc": "Establezca el número máximo de etiquetas a generar por nota."
3333+ },
3434+ "promptStyle": {
3535+ "title": "Estilo de prompt",
3636+ "desc": "Elija un estilo de prompt predefinido o cree su propio prompt personalizado.",
3737+ "standard": "Estándar",
3838+ "descriptive": "Descriptivo",
3939+ "academic": "Académico",
4040+ "concise": "Conciso",
4141+ "custom": "Personalizado"
4242+ },
4343+ "customPrompt": {
4444+ "title": "Plantilla de prompt personalizada",
4545+ "desc": "Personalice el prompt enviado a la IA. Use {maxTags} y {content} como marcadores de posición."
4646+ },
4747+ "currentPrompt": {
4848+ "title": "Plantilla de prompt actual",
4949+ "desc": "Esta es la plantilla de prompt que se utilizará (solo lectura). Cambie a Personalizado si desea editarla."
5050+ },
5151+ "language": {
5252+ "title": "Idioma",
5353+ "desc": "Elija el idioma para la interfaz del plugin."
5454+ }
5555+ },
5656+ "modals": {
5757+ "confirm": "Confirmar",
5858+ "cancel": "Cancelar",
5959+ "settings": "Abrir configuración",
6060+ "configError": "Error de configuración de etiquetado IA",
6161+ "tagAll": "Esto etiquetará todas las notas en su bóveda usando el modelo {model}. Esto puede llevar tiempo y consumir créditos de API. ¿Desea continuar?"
6262+ },
6363+ "notices": {
6464+ "noActiveNote": "No hay nota activa para etiquetar",
6565+ "analyzing": "Analizando contenido de la nota y generando etiquetas...",
6666+ "tagSuccess": "Etiquetas añadidas con éxito: {tags}",
6767+ "tagError": "Error al etiquetar nota: {error}",
6868+ "startingBulk": "Comenzando a etiquetar notas...",
6969+ "processingNote": "Procesando: {file}\nProgreso: {processed}/{total} ({successful} exitosas)",
7070+ "bulkComplete": "Etiquetado completado: {successful}/{total} notas con éxito",
7171+ "bulkError": "Error durante el etiquetado masivo: {error}"
7272+ },
7373+ "errors": {
7474+ "apiKeyMissing": "Clave API no configurada. Por favor, añada su clave API en la configuración del plugin.",
7575+ "endpointMissing": "Endpoint API personalizado no configurado. Por favor, añada su URL de endpoint en la configuración del plugin.",
7676+ "apiError": "Error al generar etiquetas: {error}",
7777+ "unknownProvider": "Proveedor desconocido: {provider}"
7878+ },
7979+ "commands": {
8080+ "tagCurrent": "Etiquetar nota actual con IA",
8181+ "tagAll": "Etiquetar todas las notas con IA"
8282+ },
8383+ "ribbon": {
8484+ "tooltip": "Auto-etiquetar con IA"
8585+ }
8686+}
+29
src/i18n/locales/index.ts
···11+import ar from "./ar.json";
22+import de from "./de.json";
33+import en from "./en.json";
44+import es from "./es.json";
55+66+export type TranslationKey = keyof typeof en;
77+88+export const locales = {
99+ ar,
1010+ de,
1111+ en,
1212+ es,
1313+};
1414+1515+export type LanguageCode = keyof typeof locales;
1616+export const languageCodes: LanguageCode[] = Object.keys(
1717+ locales
1818+) as LanguageCode[];
1919+2020+export const languageCodesList = Object.entries(locales).map(([key]) => ({
2121+ code: key,
2222+}));
2323+2424+export const languageNames = {
2525+ ar: "العربية",
2626+ de: "Deutsch",
2727+ en: "English",
2828+ es: "Español",
2929+};
+163
src/i18n/translationService.ts
···11+import { App } from "obsidian";
22+import { LanguageCode, locales } from "./locales";
33+44+class TranslationService {
55+ private currentLanguage: LanguageCode = "en"; // Default to English
66+ private app: App | null = null;
77+88+ constructor() {
99+ // Default to browser language initially
1010+ // Will be overridden by Obsidian language or user setting
1111+ const browserLang = window.navigator.language.split("-")[0] as LanguageCode;
1212+ if (this.isLanguageSupported(browserLang)) {
1313+ this.currentLanguage = browserLang;
1414+ }
1515+ }
1616+1717+ // Store bound event handler for proper cleanup
1818+ private boundCheckForLanguageChanges: (() => void) | null = null;
1919+2020+ /**
2121+ * Initialize the service with the Obsidian app instance
2222+ * This allows getting the app's configured language and listening for changes
2323+ */
2424+ public initializeApp(app: App): void {
2525+ this.app = app;
2626+2727+ // Set up event listener for when Obsidian's language changes
2828+ // Obsidian doesn't directly expose a language change event, but when the app
2929+ // locale changes, many UI elements will be updated and we can listen for those changes
3030+ this.boundCheckForLanguageChanges = this.checkForLanguageChanges.bind(this);
3131+ // @ts-expect-error
3232+ this.app.workspace.on("layout-change", this.boundCheckForLanguageChanges);
3333+ }
3434+3535+ /**
3636+ * Clean up event listeners when plugin is unloaded
3737+ */
3838+ public cleanup(): void {
3939+ if (this.app && this.boundCheckForLanguageChanges) {
4040+ this.app.workspace.off(
4141+ "layout-change",
4242+ this.boundCheckForLanguageChanges
4343+ );
4444+ this.boundCheckForLanguageChanges = null;
4545+ }
4646+ }
4747+4848+ /**
4949+ * Check if Obsidian's language has changed and update if needed
5050+ */
5151+ private checkForLanguageChanges(): void {
5252+ if (!this.app) return;
5353+5454+ const currentObsidianLang = this.getObsidianLanguage();
5555+ if (currentObsidianLang !== this.currentLanguage) {
5656+ this.setLanguage(currentObsidianLang);
5757+ }
5858+ }
5959+6060+ /**
6161+ * Get the ISO code for the currently configured Obsidian app language
6262+ * @returns The language code (defaults to 'en')
6363+ */
6464+ public getObsidianLanguage(): LanguageCode {
6565+ if (!this.app) return "en";
6666+6767+ // Get language from Obsidian
6868+ // @ts-ignore - getLanguage() exists but isn't in the type definitions
6969+ const obsidianLang = this.app.vault.getConfig("language") || "en";
7070+7171+ // Handle special cases or normalize language code if needed
7272+ const normalizedLang = this.normalizeLanguageCode(obsidianLang);
7373+7474+ return this.isLanguageSupported(normalizedLang)
7575+ ? (normalizedLang as LanguageCode)
7676+ : "en";
7777+ }
7878+7979+ /**
8080+ * Normalize language codes to match our supported formats
8181+ */
8282+ private normalizeLanguageCode(langCode: string): string {
8383+ // Handle specific language code mappings
8484+ const mappings: Record<string, string> = {
8585+ "zh-cn": "zh",
8686+ "zh-hans": "zh",
8787+ "zh-hant": "zh-TW",
8888+ "pt-pt": "pt",
8989+ "pt-br": "pt-BR",
9090+ };
9191+9292+ // Return the mapped value if it exists, otherwise return the original
9393+ return mappings[langCode.toLowerCase()] || langCode;
9494+ }
9595+9696+ /**
9797+ * Set the current language for translations
9898+ */
9999+ public setLanguage(lang: LanguageCode): void {
100100+ if (this.isLanguageSupported(lang)) {
101101+ this.currentLanguage = lang;
102102+ } else {
103103+ console.warn(
104104+ `Language ${lang} is not supported. Falling back to English.`
105105+ );
106106+ this.currentLanguage = "en"; // Fallback to English
107107+ }
108108+ }
109109+110110+ /**
111111+ * Get the current language being used for translations
112112+ */
113113+ public getCurrentLanguage(): LanguageCode {
114114+ return this.currentLanguage;
115115+ }
116116+117117+ /**
118118+ * Translate a key with optional replacements
119119+ * @param key The dot-notation key for the translation
120120+ * @param replacements An object of values to replace in the translation
121121+ * @returns The translated string
122122+ *
123123+ * Example:
124124+ * t('settings.apiKey.desc', { provider: 'OpenAI' })
125125+ */
126126+ public t(key: string, replacements?: Record<string, string>): string {
127127+ const locale = this.getLocale();
128128+129129+ // Get the translation from the nested key
130130+ const translation = key
131131+ .split(".")
132132+ .reduce(
133133+ (obj, key) => (obj && typeof obj === "object" ? obj[key] : undefined),
134134+ locale
135135+ );
136136+137137+ if (typeof translation !== "string") {
138138+ console.warn(`Translation key "${key}" not found. Falling back to key.`);
139139+ return key;
140140+ }
141141+142142+ // Replace placeholders if provided
143143+ if (replacements) {
144144+ return Object.entries(replacements).reduce(
145145+ (str, [key, value]) => str.replace(new RegExp(`{${key}}`, "g"), value),
146146+ translation
147147+ );
148148+ }
149149+150150+ return translation;
151151+ }
152152+153153+ private isLanguageSupported(lang: string): boolean {
154154+ return Object.keys(locales).includes(lang);
155155+ }
156156+157157+ private getLocale(): any {
158158+ return locales[this.currentLanguage] || locales.en;
159159+ }
160160+}
161161+162162+// Singleton instance to be used across the app
163163+export const i18n = new TranslationService();
+199
src/main.ts
···11+import { MarkdownView, Plugin } from "obsidian";
22+33+import { DEFAULT_SETTINGS } from "./models/constants";
44+import { AITaggerSettings } from "./models/types";
55+import { tagAllNotes, tagSingleNote } from "./services/noteService";
66+import { NotificationService } from "./services/notificationService";
77+import { AITaggerSettingTab } from "./ui/AITaggerSettingTab";
88+import { ConfirmModal } from "./ui/ConfirmModal";
99+import { getModelName, validateApiSettings } from "./utils/validationUtils";
1010+1111+export default class AITaggerPlugin extends Plugin {
1212+ settings: AITaggerSettings;
1313+1414+ async onload() {
1515+ await this.loadSettings();
1616+1717+ // Initialize language service with app instance and set language to Obsidian's language
1818+ const { i18n } = await import("./i18n");
1919+ i18n.initializeApp(this.app);
2020+2121+ // Always use Obsidian's language setting
2222+ const obsidianLang = i18n.getObsidianLanguage();
2323+ i18n.setLanguage(obsidianLang);
2424+2525+ // Create an icon in the left ribbon
2626+ const ribbonIconEl = this.addRibbonIcon(
2727+ "tag",
2828+ i18n.t("ribbon.tooltip"),
2929+ this.handleRibbonClick.bind(this)
3030+ );
3131+ ribbonIconEl.addClass("ai-tagger-ribbon-class");
3232+3333+ // Add command to tag current note
3434+ this.addCommand({
3535+ id: "tag-current-note",
3636+ name: i18n.t("commands.tagCurrent"),
3737+ checkCallback: this.checkAndTagCurrentNote.bind(this),
3838+ });
3939+4040+ // Add command to tag all notes
4141+ this.addCommand({
4242+ id: "tag-all-notes",
4343+ name: i18n.t("commands.tagAll"),
4444+ callback: this.confirmAndTagAllNotes.bind(this),
4545+ });
4646+4747+ // Add settings tab
4848+ this.addSettingTab(new AITaggerSettingTab(this.app, this));
4949+ }
5050+5151+ onunload() {
5252+ // Clean up the translation service's event listeners
5353+ import("./i18n").then(({ i18n }) => {
5454+ i18n.cleanup();
5555+ });
5656+ }
5757+5858+ async loadSettings() {
5959+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
6060+ }
6161+6262+ async saveSettings() {
6363+ await this.saveData(this.settings);
6464+ }
6565+6666+ private handleRibbonClick() {
6767+ const validation = validateApiSettings(this.settings);
6868+6969+ if (!validation.valid) {
7070+ new ConfirmModal(
7171+ this.app,
7272+ "AI tagging configuration error",
7373+ () => {
7474+ null;
7575+ },
7676+ true,
7777+ validation.error
7878+ ).open();
7979+ return;
8080+ }
8181+8282+ // Tag the current note directly when clicking the ribbon icon
8383+ this.tagCurrentNote();
8484+ }
8585+8686+ private checkAndTagCurrentNote(checking: boolean): boolean {
8787+ const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
8888+8989+ if (!markdownView) {
9090+ return false;
9191+ }
9292+9393+ if (checking) {
9494+ return true;
9595+ }
9696+9797+ const validation = validateApiSettings(this.settings);
9898+9999+ if (!validation.valid) {
100100+ new ConfirmModal(
101101+ this.app,
102102+ "AI tagging configuration error",
103103+ () => {
104104+ null;
105105+ },
106106+ true,
107107+ validation.error
108108+ ).open();
109109+ return true;
110110+ }
111111+112112+ this.tagCurrentNote();
113113+ return true;
114114+ }
115115+116116+ private confirmAndTagAllNotes() {
117117+ const validation = validateApiSettings(this.settings);
118118+119119+ if (!validation.valid) {
120120+ new ConfirmModal(
121121+ this.app,
122122+ "AI tagging configuration error",
123123+ () => {
124124+ null;
125125+ },
126126+ true,
127127+ validation.error
128128+ ).open();
129129+ return;
130130+ }
131131+132132+ const modelName = getModelName(this.settings);
133133+134134+ new ConfirmModal(
135135+ this.app,
136136+ `This will tag all notes in your vault using the ${modelName} model. This may take a while and consume API credits. Do you want to continue?`,
137137+ () => this.tagAllNotes()
138138+ ).open();
139139+ }
140140+141141+ private async tagCurrentNote() {
142142+ const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
143143+ if (!activeView || !activeView.file) {
144144+ NotificationService.showNotice("No active note to tag");
145145+ return;
146146+ }
147147+148148+ const file = activeView.file;
149149+150150+ // Create a persistent notice
151151+ const notification = NotificationService.showPersistentNotice(
152152+ "Analyzing note content and generating tags..."
153153+ );
154154+155155+ try {
156156+ const result = await tagSingleNote(this.app, file, this.settings);
157157+158158+ if (result.success) {
159159+ notification.setSuccess(
160160+ `Successfully added tags: ${result.tags.join(", ")}`
161161+ );
162162+ } else {
163163+ notification.setError(`Error tagging note: ${result.error}`);
164164+ }
165165+ } catch (error) {
166166+ console.error("Error tagging note:", error);
167167+ const errorMessage =
168168+ error instanceof Error ? error.message : String(error);
169169+ notification.setError(`Error tagging note: ${errorMessage}`);
170170+ }
171171+ }
172172+173173+ private async tagAllNotes() {
174174+ // Create a persistent notice that we'll update
175175+ const notification = NotificationService.showPersistentNotice(
176176+ "Starting to tag notes..."
177177+ );
178178+179179+ try {
180180+ const result = await tagAllNotes(
181181+ this.app,
182182+ this.settings,
183183+ (processed, successful, total, currentFile) => {
184184+ notification.setProgress(processed, successful, total, currentFile);
185185+ }
186186+ );
187187+188188+ // Final notice with completion message
189189+ notification.setSuccess(
190190+ `Completed tagging ${result.successful}/${result.total} notes successfully`
191191+ );
192192+ } catch (error) {
193193+ console.error("Error during bulk tagging:", error);
194194+ const errorMessage =
195195+ error instanceof Error ? error.message : String(error);
196196+ notification.setError(`Error during bulk tagging: ${errorMessage}`);
197197+ }
198198+ }
199199+}
+83
src/models/constants.ts
···11+import {
22+ AIProvider,
33+ AITaggerSettings,
44+ ProviderConfig,
55+ ServiceTypeEnum,
66+} from "./types";
77+88+// Define prompt templates
99+export const PROMPT_TEMPLATES = {
1010+ standard:
1111+ "Generate {maxTags} relevant tags for the following note content. Return only the tags as a comma-separated list, without any additional commentary. Tags should be lowercase and use hyphens for multi-word tags.\n\nContent:\n{content}",
1212+ descriptive:
1313+ "Analyze the following note content and generate {maxTags} descriptive tags that capture the main topics, concepts, and themes. Return only the tags as a comma-separated list, without any additional commentary. Tags should be lowercase and use hyphens for multi-word tags.\n\nContent:\n{content}",
1414+ academic:
1515+ "Review the following academic or research note and generate {maxTags} specific tags that would help categorize this content in an academic context. Include relevant field-specific terminology. Return only the tags as a comma-separated list, without any additional commentary. Tags should be lowercase and use hyphens for multi-word tags.\n\nContent:\n{content}",
1616+ concise:
1717+ "Generate {maxTags} short, concise tags for the following note content. Focus on single-word tags when possible. Return only the tags as a comma-separated list, without any additional commentary. Tags should be lowercase.\n\nContent:\n{content}",
1818+ custom: "",
1919+};
2020+2121+// Define model configurations for each provider
2222+export const MODEL_CONFIGS: Record<AIProvider, ProviderConfig> = {
2323+ [AIProvider.Anthropic]: {
2424+ apiUrl: "https://api.anthropic.com/v1/messages",
2525+ models: [
2626+ { id: "claude-3-5-sonnet-20240620", name: "Claude 3.5 Sonnet" },
2727+ { id: "claude-3-opus-20240229", name: "Claude 3 Opus" },
2828+ { id: "claude-3-sonnet-20240229", name: "Claude 3 Sonnet" },
2929+ { id: "claude-3-haiku-20240307", name: "Claude 3 Haiku" },
3030+ ],
3131+ defaultModel: "claude-3-5-sonnet-20240620",
3232+ apiKeyUrl: "https://console.anthropic.com/",
3333+ },
3434+ [AIProvider.OpenAI]: {
3535+ apiUrl: "https://api.openai.com/v1/chat/completions",
3636+ models: [
3737+ { id: "gpt-4o", name: "GPT-4o" },
3838+ { id: "gpt-4-turbo", name: "GPT-4 Turbo" },
3939+ { id: "gpt-4", name: "GPT-4" },
4040+ { id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo" },
4141+ ],
4242+ defaultModel: "gpt-3.5-turbo",
4343+ apiKeyUrl: "https://platform.openai.com/api-keys",
4444+ },
4545+ [AIProvider.Mistral]: {
4646+ apiUrl: "https://api.mistral.ai/v1/chat/completions",
4747+ models: [
4848+ { id: "mistral-large-latest", name: "Mistral Large" },
4949+ { id: "mistral-medium-latest", name: "Mistral Medium" },
5050+ { id: "mistral-small-latest", name: "Mistral Small" },
5151+ { id: "open-mistral-7b", name: "Open Mistral 7B" },
5252+ ],
5353+ defaultModel: "mistral-small-latest",
5454+ apiKeyUrl: "https://console.mistral.ai/api-keys/",
5555+ },
5656+ [AIProvider.Google]: {
5757+ apiUrl: "https://generativelanguage.googleapis.com/v1beta/models/",
5858+ models: [
5959+ { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" },
6060+ { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" },
6161+ { id: "gemini-1.0-pro", name: "Gemini 1.0 Pro" },
6262+ ],
6363+ defaultModel: "gemini-1.5-flash",
6464+ apiKeyUrl: "https://aistudio.google.com/app/apikey",
6565+ },
6666+ [AIProvider.Custom]: {
6767+ apiUrl: "",
6868+ models: [{ id: "custom-model", name: "Custom Model" }],
6969+ defaultModel: "custom-model",
7070+ apiKeyUrl: "",
7171+ },
7272+};
7373+7474+export const DEFAULT_SETTINGS: AITaggerSettings = {
7575+ serviceType: ServiceTypeEnum.CLOUD,
7676+ provider: AIProvider.Anthropic,
7777+ apiKey: "",
7878+ modelName: MODEL_CONFIGS[AIProvider.Anthropic].defaultModel,
7979+ maxTags: 5,
8080+ promptOption: "standard",
8181+ promptTemplate: PROMPT_TEMPLATES.standard,
8282+ customEndpoint: "",
8383+};