Auto tagging obsidian notes w/ AI
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add multi-language support

- Add internationalization (i18n) framework
- Create English and Spanish language files as examples
- Implement automatic language detection based on browser settings
- Add language selection to settings tab
- Refactor all UI text to use translation system
- Support placeholders in translations for dynamic content

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>

+389 -53
+2
src/i18n/index.ts
··· 1 + export { i18n } from './translationService'; 2 + export { languageNames } from './locales';
+86
src/i18n/locales/en.json
··· 1 + { 2 + "settings": { 3 + "title": "AI Tagger Settings", 4 + "provider": { 5 + "title": "AI Provider", 6 + "desc": "Select which AI provider to use for generating tags.", 7 + "anthropic": "Anthropic (Claude)", 8 + "openai": "OpenAI (GPT)", 9 + "mistral": "Mistral AI", 10 + "google": "Google (Gemini)", 11 + "custom": "Custom Endpoint (OpenAI Compatible)" 12 + }, 13 + "customEndpoint": { 14 + "title": "Custom API Endpoint", 15 + "desc": "Enter the URL for your custom OpenAI-compatible API endpoint.", 16 + "placeholder": "https://your-api-endpoint.com/v1/chat/completions" 17 + }, 18 + "apiKey": { 19 + "title": "API Key", 20 + "desc": "Your {provider} API key. Required to use the AI service.", 21 + "getKey": "Get it from {url} if you don't have one already.", 22 + "recommendation": "We recommend using a dedicated key for this plugin.", 23 + "placeholder": "Enter your API key" 24 + }, 25 + "model": { 26 + "title": "AI Model", 27 + "desc": "Choose which AI model to use for tag generation.", 28 + "custom": "{model} (Custom)" 29 + }, 30 + "maxTags": { 31 + "title": "Maximum number of tags", 32 + "desc": "Set the maximum number of tags to generate per note." 33 + }, 34 + "promptStyle": { 35 + "title": "Prompt Style", 36 + "desc": "Choose a predefined prompt style or create your own custom prompt.", 37 + "standard": "Standard", 38 + "descriptive": "Descriptive", 39 + "academic": "Academic", 40 + "concise": "Concise", 41 + "custom": "Custom" 42 + }, 43 + "customPrompt": { 44 + "title": "Custom prompt template", 45 + "desc": "Customize the prompt sent to the AI. Use {maxTags} and {content} as placeholders." 46 + }, 47 + "currentPrompt": { 48 + "title": "Current prompt template", 49 + "desc": "This is the prompt template that will be used (read-only). Switch to Custom if you want to edit it." 50 + }, 51 + "language": { 52 + "title": "Language", 53 + "desc": "Choose the language for the plugin interface." 54 + } 55 + }, 56 + "modals": { 57 + "confirm": "Confirm", 58 + "cancel": "Cancel", 59 + "settings": "Open settings", 60 + "configError": "AI tagging configuration error", 61 + "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?" 62 + }, 63 + "notices": { 64 + "noActiveNote": "No active note to tag", 65 + "analyzing": "Analyzing note content and generating tags...", 66 + "tagSuccess": "Successfully added tags: {tags}", 67 + "tagError": "Error tagging note: {error}", 68 + "startingBulk": "Starting to tag notes...", 69 + "processingNote": "Processing: {file}\nProgress: {processed}/{total} ({successful} successful)", 70 + "bulkComplete": "Completed tagging {successful}/{total} notes successfully", 71 + "bulkError": "Error during bulk tagging: {error}" 72 + }, 73 + "errors": { 74 + "apiKeyMissing": "API key not configured. Please add your API key in the plugin settings.", 75 + "endpointMissing": "Custom API endpoint not configured. Please add your endpoint URL in the plugin settings.", 76 + "apiError": "Failed to generate tags: {error}", 77 + "unknownProvider": "Unknown provider: {provider}" 78 + }, 79 + "commands": { 80 + "tagCurrent": "Tag current note with AI", 81 + "tagAll": "Tag all notes with AI" 82 + }, 83 + "ribbon": { 84 + "tooltip": "Auto-tag with AI" 85 + } 86 + }
+86
src/i18n/locales/es.json
··· 1 + { 2 + "settings": { 3 + "title": "Configuración de AI Tagger", 4 + "provider": { 5 + "title": "Proveedor de IA", 6 + "desc": "Seleccione qué proveedor de IA usar para generar etiquetas.", 7 + "anthropic": "Anthropic (Claude)", 8 + "openai": "OpenAI (GPT)", 9 + "mistral": "Mistral AI", 10 + "google": "Google (Gemini)", 11 + "custom": "Endpoint personalizado (Compatible con OpenAI)" 12 + }, 13 + "customEndpoint": { 14 + "title": "Endpoint API Personalizado", 15 + "desc": "Ingrese la URL para su endpoint API compatible con OpenAI.", 16 + "placeholder": "https://your-api-endpoint.com/v1/chat/completions" 17 + }, 18 + "apiKey": { 19 + "title": "Clave API", 20 + "desc": "Su clave API de {provider}. Necesaria para usar el servicio de IA.", 21 + "getKey": "Obténgala desde {url} si aún no tiene una.", 22 + "recommendation": "Recomendamos usar una clave dedicada para este plugin.", 23 + "placeholder": "Ingrese su clave API" 24 + }, 25 + "model": { 26 + "title": "Modelo de IA", 27 + "desc": "Elija qué modelo de IA usar para generar etiquetas.", 28 + "custom": "{model} (Personalizado)" 29 + }, 30 + "maxTags": { 31 + "title": "Número máximo de etiquetas", 32 + "desc": "Establezca el número máximo de etiquetas a generar por nota." 33 + }, 34 + "promptStyle": { 35 + "title": "Estilo de prompt", 36 + "desc": "Elija un estilo de prompt predefinido o cree su propio prompt personalizado.", 37 + "standard": "Estándar", 38 + "descriptive": "Descriptivo", 39 + "academic": "Académico", 40 + "concise": "Conciso", 41 + "custom": "Personalizado" 42 + }, 43 + "customPrompt": { 44 + "title": "Plantilla de prompt personalizada", 45 + "desc": "Personalice el prompt enviado a la IA. Use {maxTags} y {content} como marcadores de posición." 46 + }, 47 + "currentPrompt": { 48 + "title": "Plantilla de prompt actual", 49 + "desc": "Esta es la plantilla de prompt que se utilizará (solo lectura). Cambie a Personalizado si desea editarla." 50 + }, 51 + "language": { 52 + "title": "Idioma", 53 + "desc": "Elija el idioma para la interfaz del plugin." 54 + } 55 + }, 56 + "modals": { 57 + "confirm": "Confirmar", 58 + "cancel": "Cancelar", 59 + "settings": "Abrir configuración", 60 + "configError": "Error de configuración de etiquetado IA", 61 + "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?" 62 + }, 63 + "notices": { 64 + "noActiveNote": "No hay nota activa para etiquetar", 65 + "analyzing": "Analizando contenido de la nota y generando etiquetas...", 66 + "tagSuccess": "Etiquetas añadidas con éxito: {tags}", 67 + "tagError": "Error al etiquetar nota: {error}", 68 + "startingBulk": "Comenzando a etiquetar notas...", 69 + "processingNote": "Procesando: {file}\nProgreso: {processed}/{total} ({successful} exitosas)", 70 + "bulkComplete": "Etiquetado completado: {successful}/{total} notas con éxito", 71 + "bulkError": "Error durante el etiquetado masivo: {error}" 72 + }, 73 + "errors": { 74 + "apiKeyMissing": "Clave API no configurada. Por favor, añada su clave API en la configuración del plugin.", 75 + "endpointMissing": "Endpoint API personalizado no configurado. Por favor, añada su URL de endpoint en la configuración del plugin.", 76 + "apiError": "Error al generar etiquetas: {error}", 77 + "unknownProvider": "Proveedor desconocido: {provider}" 78 + }, 79 + "commands": { 80 + "tagCurrent": "Etiquetar nota actual con IA", 81 + "tagAll": "Etiquetar todas las notas con IA" 82 + }, 83 + "ribbon": { 84 + "tooltip": "Auto-etiquetar con IA" 85 + } 86 + }
+16
src/i18n/locales/index.ts
··· 1 + import en from './en.json'; 2 + import es from './es.json'; 3 + 4 + export type TranslationKey = keyof typeof en; 5 + 6 + export const locales = { 7 + en, 8 + es, 9 + // Add more languages here 10 + }; 11 + 12 + export const languageNames = { 13 + en: "English", 14 + es: "Español", 15 + // Add more language names here 16 + };
+72
src/i18n/translationService.ts
··· 1 + import { locales } from './locales'; 2 + import { Language } from '../models/types'; 3 + 4 + class TranslationService { 5 + private currentLanguage: Language = 'en'; 6 + 7 + constructor() { 8 + // Use browser language as default if supported 9 + const browserLang = window.navigator.language.split('-')[0] as Language; 10 + if (this.isLanguageSupported(browserLang)) { 11 + this.currentLanguage = browserLang; 12 + } 13 + } 14 + 15 + public setLanguage(lang: Language): void { 16 + if (this.isLanguageSupported(lang)) { 17 + this.currentLanguage = lang; 18 + } else { 19 + console.warn(`Language ${lang} is not supported. Falling back to English.`); 20 + this.currentLanguage = 'en'; 21 + } 22 + } 23 + 24 + public getCurrentLanguage(): Language { 25 + return this.currentLanguage; 26 + } 27 + 28 + /** 29 + * Translate a key with optional replacements 30 + * @param key The dot-notation key for the translation 31 + * @param replacements An object of values to replace in the translation 32 + * @returns The translated string 33 + * 34 + * Example: 35 + * t('settings.apiKey.desc', { provider: 'OpenAI' }) 36 + */ 37 + public t(key: string, replacements?: Record<string, string>): string { 38 + const locale = this.getLocale(); 39 + 40 + // Get the translation from the nested key 41 + const translation = key.split('.').reduce((obj, key) => 42 + obj && typeof obj === 'object' ? obj[key] : undefined, 43 + locale as any 44 + ); 45 + 46 + if (typeof translation !== 'string') { 47 + console.warn(`Translation key "${key}" not found. Falling back to key.`); 48 + return key; 49 + } 50 + 51 + // Replace placeholders if provided 52 + if (replacements) { 53 + return Object.entries(replacements).reduce( 54 + (str, [key, value]) => str.replace(new RegExp(`{${key}}`, 'g'), value), 55 + translation 56 + ); 57 + } 58 + 59 + return translation; 60 + } 61 + 62 + private isLanguageSupported(lang: string): boolean { 63 + return Object.keys(locales).includes(lang); 64 + } 65 + 66 + private getLocale(): any { 67 + return locales[this.currentLanguage] || locales.en; 68 + } 69 + } 70 + 71 + // Singleton instance to be used across the app 72 + export const i18n = new TranslationService();
+8 -3
src/main.ts
··· 18 18 19 19 async onload() { 20 20 await this.loadSettings(); 21 + 22 + // Initialize language from settings 23 + import('./i18n').then(({ i18n }) => { 24 + i18n.setLanguage(this.settings.language); 25 + }); 21 26 22 27 // Create an icon in the left ribbon 23 28 const ribbonIconEl = this.addRibbonIcon( 24 29 "tag", 25 - "Auto-tag with AI", 30 + i18n.t("ribbon.tooltip"), 26 31 this.handleRibbonClick.bind(this) 27 32 ); 28 33 ribbonIconEl.addClass("ai-tagger-ribbon-class"); ··· 30 35 // Add command to tag current note 31 36 this.addCommand({ 32 37 id: "tag-current-note", 33 - name: "Tag current note with AI", 38 + name: i18n.t("commands.tagCurrent"), 34 39 checkCallback: this.checkAndTagCurrentNote.bind(this), 35 40 }); 36 41 37 42 // Add command to tag all notes 38 43 this.addCommand({ 39 44 id: "tag-all-notes", 40 - name: "Tag all notes with AI", 45 + name: i18n.t("commands.tagAll"), 41 46 callback: this.confirmAndTagAllNotes.bind(this), 42 47 }); 43 48
+1
src/models/constants.ts
··· 74 74 promptOption: "standard", 75 75 promptTemplate: PROMPT_TEMPLATES.standard, 76 76 customEndpoint: "", 77 + language: "en", 77 78 };
+26 -1
src/models/types.ts
··· 8 8 Custom = "custom", 9 9 } 10 10 11 + export type LanguageCode = 12 + | "ar" // Arabic 13 + | "cs" // Czech 14 + | "da" // Danish 15 + | "de" // German 16 + | "en" // English 17 + | "es" // Spanish 18 + | "fr" // French 19 + | "id" // Indonesian 20 + | "it" // Italian 21 + | "ja" // Japanese 22 + | "ko" // Korean 23 + | "nl" // Dutch 24 + | "no" // Norwegian 25 + | "pl" // Polish 26 + | "pt" // Portuguese 27 + | "pt-BR" // Brazilian Portuguese 28 + | "ro" // Romanian 29 + | "ru" // Russian 30 + | "tr" // Turkish 31 + | "uk" // Ukrainian 32 + | "zh" // Chinese (Simplified) 33 + | "zh-TW"; // Chinese (Traditional) 34 + 11 35 export interface AITaggerSettings { 12 36 provider: AIProvider; 13 37 apiKey: string; ··· 16 40 promptOption: string; 17 41 promptTemplate: string; 18 42 customEndpoint: string; 43 + language: LanguageCode; 19 44 } 20 45 21 46 export interface ModelInfo { ··· 35 60 tags: string[]; 36 61 success: boolean; 37 62 error?: string; 38 - } 63 + }
+1
src/services/notificationService.ts
··· 1 1 import { Notice } from "obsidian"; 2 + import { i18n } from "../i18n"; 2 3 3 4 interface PersistentNoticeOptions { 4 5 duration?: number;
+25
src/services/tagGenerator.ts
··· 8 8 ): Promise<string[]> { 9 9 validateSettings(settings); 10 10 11 + const languageNames: Record<string, string> = { 12 + ar: "Arabic", 13 + cs: "Czech", 14 + da: "Danish", 15 + de: "German", 16 + en: "English", 17 + es: "Spanish", 18 + fr: "French", 19 + id: "Indonesian", 20 + it: "Italian", 21 + ja: "Japanese", 22 + ko: "Korean", 23 + nl: "Dutch", 24 + no: "Norwegian", 25 + pl: "Polish", 26 + pt: "Portuguese", 27 + "pt-BR": "Brazilian Portuguese", 28 + ro: "Romanian", 29 + ru: "Russian", 30 + tr: "Turkish", 31 + uk: "Ukrainian", 32 + zh: "Chinese (Simplified)", 33 + "zh-TW": "Chinese (Traditional)", 34 + }; 35 + 11 36 // Replace placeholders in the prompt template 12 37 const prompt = settings.promptTemplate 13 38 .replace("{maxTags}", settings.maxTags.toString())
+61 -45
src/ui/AITaggerSettingTab.ts
··· 2 2 import { AIProvider } from "../models/types"; 3 3 import { MODEL_CONFIGS, PROMPT_TEMPLATES } from "../models/constants"; 4 4 import AITaggerPlugin from "../main"; 5 + import { i18n, languageNames } from "../i18n"; 5 6 6 7 export class AITaggerSettingTab extends PluginSettingTab { 7 8 plugin: AITaggerPlugin; ··· 15 16 const { containerEl } = this; 16 17 containerEl.empty(); 17 18 19 + this.addLanguageSection(containerEl); 18 20 this.addProviderSection(containerEl); 19 21 this.addApiSection(containerEl); 20 22 this.addTaggingOptionsSection(containerEl); 21 23 this.addPromptSection(containerEl); 22 24 } 25 + 26 + private addLanguageSection(containerEl: HTMLElement): void { 27 + new Setting(containerEl) 28 + .setName(i18n.t("settings.language.title")) 29 + .setDesc(i18n.t("settings.language.desc")) 30 + .addDropdown((dropdown) => { 31 + // Add all supported languages 32 + Object.entries(languageNames).forEach(([code, name]) => { 33 + dropdown.addOption(code, name); 34 + }); 35 + 36 + dropdown 37 + .setValue(this.plugin.settings.language) 38 + .onChange(async (value) => { 39 + this.plugin.settings.language = value; 40 + i18n.setLanguage(value); 41 + await this.plugin.saveSettings(); 42 + this.display(); // Refresh the UI with the new language 43 + }); 44 + return dropdown; 45 + }); 46 + } 23 47 24 48 private addProviderSection(containerEl: HTMLElement): void { 25 49 // AI Provider selection 26 50 new Setting(containerEl) 27 - .setName("AI Provider") 28 - .setDesc("Select which AI provider to use for generating tags.") 51 + .setName(i18n.t("settings.provider.title")) 52 + .setDesc(i18n.t("settings.provider.desc")) 29 53 .addDropdown((dropdown) => { 30 54 dropdown 31 - .addOption(AIProvider.Anthropic, "Anthropic (Claude)") 32 - .addOption(AIProvider.OpenAI, "OpenAI (GPT)") 33 - .addOption(AIProvider.Mistral, "Mistral AI") 34 - .addOption(AIProvider.Google, "Google (Gemini)") 35 - .addOption(AIProvider.Custom, "Custom Endpoint (OpenAI Compatible)") 55 + .addOption(AIProvider.Anthropic, i18n.t("settings.provider.anthropic")) 56 + .addOption(AIProvider.OpenAI, i18n.t("settings.provider.openai")) 57 + .addOption(AIProvider.Mistral, i18n.t("settings.provider.mistral")) 58 + .addOption(AIProvider.Google, i18n.t("settings.provider.google")) 59 + .addOption(AIProvider.Custom, i18n.t("settings.provider.custom")) 36 60 .setValue(this.plugin.settings.provider) 37 61 .onChange(async (value) => { 38 62 const newProvider = value as AIProvider; ··· 53 77 // Custom endpoint setting (only shown for custom provider) 54 78 if (this.plugin.settings.provider === AIProvider.Custom) { 55 79 new Setting(containerEl) 56 - .setName("Custom API Endpoint") 57 - .setDesc( 58 - "Enter the URL for your custom OpenAI-compatible API endpoint." 59 - ) 80 + .setName(i18n.t("settings.customEndpoint.title")) 81 + .setDesc(i18n.t("settings.customEndpoint.desc")) 60 82 .addText((text) => 61 83 text 62 - .setPlaceholder("https://your-api-endpoint.com/v1/chat/completions") 84 + .setPlaceholder(i18n.t("settings.customEndpoint.placeholder")) 63 85 .setValue(this.plugin.settings.customEndpoint) 64 86 .onChange(async (value) => { 65 87 this.plugin.settings.customEndpoint = value; ··· 74 96 const providerConfig = MODEL_CONFIGS[this.plugin.settings.provider]; 75 97 76 98 // API Key with provider-specific description 99 + const providerName = this.plugin.settings.provider === AIProvider.Custom 100 + ? "" 101 + : this.plugin.settings.provider; 102 + 77 103 new Setting(containerEl) 78 - .setName("API key") 104 + .setName(i18n.t("settings.apiKey.title")) 79 105 .setDesc( 80 - `Your ${ 81 - this.plugin.settings.provider === AIProvider.Custom 82 - ? "" 83 - : this.plugin.settings.provider 84 - } API key. Required to use the AI service. ${ 85 - providerConfig.apiKeyUrl 86 - ? `Get it from ${providerConfig.apiKeyUrl} if you don't have one already.` 87 - : "" 88 - } We recommend using a dedicated key for this plugin.` 106 + i18n.t("settings.apiKey.desc", { provider: providerName }) + " " + 107 + (providerConfig.apiKeyUrl 108 + ? i18n.t("settings.apiKey.getKey", { url: providerConfig.apiKeyUrl }) 109 + : "") + " " + 110 + i18n.t("settings.apiKey.recommendation") 89 111 ) 90 112 .addText((text) => 91 113 text 92 - .setPlaceholder("Enter your API key") 114 + .setPlaceholder(i18n.t("settings.apiKey.placeholder")) 93 115 .setValue(this.plugin.settings.apiKey) 94 116 .onChange(async (value) => { 95 117 this.plugin.settings.apiKey = value; ··· 99 121 100 122 // AI model selection (provider-specific) 101 123 new Setting(containerEl) 102 - .setName("AI model") 103 - .setDesc("Choose which AI model to use for tag generation.") 124 + .setName(i18n.t("settings.model.title")) 125 + .setDesc(i18n.t("settings.model.desc")) 104 126 .addDropdown((dropdown) => { 105 127 // Add models for the selected provider 106 128 providerConfig.models.forEach((model) => { ··· 115 137 ) { 116 138 dropdown.addOption( 117 139 this.plugin.settings.modelName, 118 - this.plugin.settings.modelName + " (Custom)" 140 + i18n.t("settings.model.custom", { model: this.plugin.settings.modelName }) 119 141 ); 120 142 } 121 143 ··· 132 154 133 155 private addTaggingOptionsSection(containerEl: HTMLElement): void { 134 156 new Setting(containerEl) 135 - .setName("Maximum number of tags") 136 - .setDesc("Set the maximum number of tags to generate per note.") 157 + .setName(i18n.t("settings.maxTags.title")) 158 + .setDesc(i18n.t("settings.maxTags.desc")) 137 159 .addSlider((slider) => 138 160 slider 139 161 .setLimits(1, 20, 1) ··· 149 171 private addPromptSection(containerEl: HTMLElement): void { 150 172 // Prompt option dropdown 151 173 new Setting(containerEl) 152 - .setName("Prompt style") 153 - .setDesc( 154 - "Choose a predefined prompt style or create your own custom prompt." 155 - ) 174 + .setName(i18n.t("settings.promptStyle.title")) 175 + .setDesc(i18n.t("settings.promptStyle.desc")) 156 176 .addDropdown((dropdown) => { 157 177 dropdown 158 - .addOption("standard", "Standard") 159 - .addOption("descriptive", "Descriptive") 160 - .addOption("academic", "Academic") 161 - .addOption("concise", "Concise") 162 - .addOption("custom", "Custom") 178 + .addOption("standard", i18n.t("settings.promptStyle.standard")) 179 + .addOption("descriptive", i18n.t("settings.promptStyle.descriptive")) 180 + .addOption("academic", i18n.t("settings.promptStyle.academic")) 181 + .addOption("concise", i18n.t("settings.promptStyle.concise")) 182 + .addOption("custom", i18n.t("settings.promptStyle.custom")) 163 183 .setValue(this.plugin.settings.promptOption) 164 184 .onChange(async (value) => { 165 185 this.plugin.settings.promptOption = value; ··· 185 205 // Only show prompt template textarea if custom option is selected 186 206 if (this.plugin.settings.promptOption === "custom") { 187 207 new Setting(containerEl) 188 - .setName("Custom prompt template") 189 - .setDesc( 190 - "Customize the prompt sent to the AI. Use {maxTags} and {content} as placeholders." 191 - ) 208 + .setName(i18n.t("settings.customPrompt.title")) 209 + .setDesc(i18n.t("settings.customPrompt.desc")) 192 210 .addTextArea((textarea) => 193 211 textarea 194 212 .setValue(this.plugin.settings.promptTemplate) ··· 201 219 } else { 202 220 // Show the current template as read-only if not using custom 203 221 new Setting(containerEl) 204 - .setName("Current prompt template") 205 - .setDesc( 206 - "This is the prompt template that will be used (read-only). Switch to Custom if you want to edit it." 207 - ) 222 + .setName(i18n.t("settings.currentPrompt.title")) 223 + .setDesc(i18n.t("settings.currentPrompt.desc")) 208 224 .addTextArea((textarea) => { 209 225 textarea 210 226 .setValue(this.plugin.settings.promptTemplate)
+5 -4
src/ui/ConfirmModal.ts
··· 1 1 import { App, Modal } from "obsidian"; 2 + import { i18n } from "../i18n"; 2 3 3 4 export class ConfirmModal extends Modal { 4 5 private onConfirmCallback: () => void; ··· 33 34 buttonContainer.addClass("ai-tagger-modal-buttons"); 34 35 35 36 const settingsButton = buttonContainer.createEl("button", { 36 - text: "Open settings", 37 + text: i18n.t("modals.settings"), 37 38 }); 38 39 settingsButton.addEventListener("click", () => { 39 40 this.close(); ··· 50 51 }); 51 52 52 53 const cancelButton = buttonContainer.createEl("button", { 53 - text: "Cancel", 54 + text: i18n.t("modals.cancel"), 54 55 }); 55 56 cancelButton.addEventListener("click", () => { 56 57 this.close(); ··· 60 61 buttonContainer.addClass("ai-tagger-modal-buttons"); 61 62 62 63 const confirmButton = buttonContainer.createEl("button", { 63 - text: "Confirm", 64 + text: i18n.t("modals.confirm"), 64 65 }); 65 66 confirmButton.addEventListener("click", () => { 66 67 this.onConfirmCallback(); ··· 68 69 }); 69 70 70 71 const cancelButton = buttonContainer.createEl("button", { 71 - text: "Cancel", 72 + text: i18n.t("modals.cancel"), 72 73 }); 73 74 cancelButton.addEventListener("click", () => { 74 75 this.close();