From 5bc95564855c70bebd8e88c26aaef5349ee7b710 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Wed, 24 Jun 2026 19:28:51 +0500 Subject: [PATCH] Add multilingual support for translation plugin (#9) - Introduced French, Indonesian, Italian, Japanese, Korean, Portuguese, Simplified Chinese, and Traditional Chinese language files for user interface localization. - Updated translation registry to include new languages. - Refactored translation functions to utilize localized strings for various UI elements, including ribbon tooltips, commands, and settings. - Enhanced settings tab to display localized text for API key, balance, output behavior, and glossary management. - Improved user notifications for translation processes, errors, and API key requirements with localized messages. - Added CSS class for inline links to improve UI consistency. - Updated TypeScript configuration to support JSON module imports. - Incremented version in versions.json to reflect new features. --- CHANGELOG.md | 9 ++ README.md | 30 +++++++ manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/LanguageSuggestModal.ts | 11 ++- src/i18n/de.json | 42 ++++++++++ src/i18n/en.json | 42 ++++++++++ src/i18n/es.json | 42 ++++++++++ src/i18n/fr.json | 42 ++++++++++ src/i18n/id.json | 42 ++++++++++ src/i18n/index.ts | 160 ++++++++++++++++++++++++++++++++++++ src/i18n/it.json | 42 ++++++++++ src/i18n/ja.json | 42 ++++++++++ src/i18n/ko.json | 42 ++++++++++ src/i18n/pt.json | 42 ++++++++++ src/i18n/zh-CN.json | 42 ++++++++++ src/i18n/zh-TW.json | 42 ++++++++++ src/main.ts | 15 ++-- src/settings.ts | 74 ++++++++++------- src/translator.ts | 33 +++++--- styles.css | 4 + tsconfig.json | 3 +- versions.json | 3 +- 24 files changed, 754 insertions(+), 58 deletions(-) create mode 100644 src/i18n/de.json create mode 100644 src/i18n/en.json create mode 100644 src/i18n/es.json create mode 100644 src/i18n/fr.json create mode 100644 src/i18n/id.json create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/it.json create mode 100644 src/i18n/ja.json create mode 100644 src/i18n/ko.json create mode 100644 src/i18n/pt.json create mode 100644 src/i18n/zh-CN.json create mode 100644 src/i18n/zh-TW.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 45aacf8..eb99f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] — 2026-06-24 + +- The plugin interface is translated into 11 languages +- Added link to manage saved linguistic Instructions + +## [1.1.2] — 2026-06-16 + +- Added support json, yaml, other plain-text formats + ## [1.1.1] — 2026-06-15 - Set correct min API version diff --git a/README.md b/README.md index d658ae5..a633509 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,36 @@ When **Generate & save glossary** is enabled (config) it automatically builds a Manage your saved glossaries at [l10n.dev/ws/translation-glossary](https://l10n.dev/ws/translation-glossary). +## Linguistic Instructions + +Linguistic Instructions let you guide AI: + 📝 "Use formal tone" + 📝 "Do not translate product names" + 📝 "Use active voice" + +Unlike glossaries that control specific terms, Linguistic Instructions control the overall style, tone, and translation behavior. +Combined with AI Glossaries, they give much more control over localization quality and brand consistency. + +Manage your saved linguistic Instructions at [l10n.dev/ws/linguistic-instructions](https://l10n.dev/ws/linguistic-instructions). + +## Localization + +The plugin interface is translated into the following languages: + +| Code | Language | +|------|----------| +| `en` | English (default) | +| `de` | German | +| `es` | Spanish | +| `fr` | French | +| `id` | Indonesian | +| `it` | Italian | +| `ja` | Japanese | +| `ko` | Korean | +| `pt` | Portuguese (Brazil) | +| `zh-CN` | Chinese (Simplified) | +| `zh-TW` | Chinese (Traditional) | + ## Privacy Translation requests are sent to the [AI translation API](https://api.l10n.dev/doc/#l10n-api-latest/tag/ai-translation) over HTTPS. l10n.dev does not store your content after translation. See the [l10n.dev terms of service](https://l10n.dev/terms-of-service) for details. diff --git a/manifest.json b/manifest.json index 6025c95..2bf8d0c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "note-ai-translator", "name": "L10n.dev - AI Translator", - "version": "1.1.2", + "version": "1.2.0", "minAppVersion": "1.1.7", "description": "Translate notes into any of 165 languages with AI. Preserves Markdown formatting and structure. Powered by L10n.dev's AI translation API. Supports json, yaml, other plain-text formats.", "author": "l10n.dev", diff --git a/package-lock.json b/package-lock.json index f37389b..316725a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-plugin-ai-translator", - "version": "1.1.2", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-plugin-ai-translator", - "version": "1.1.2", + "version": "1.2.0", "license": "MIT", "dependencies": { "obsidian": "latest" diff --git a/package.json b/package.json index c1d78fc..0d70a74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-plugin-ai-translator", - "version": "1.1.2", + "version": "1.2.0", "description": "Obsidian plugin to translate notes with AI using l10n.dev", "main": "main.js", "type": "module", diff --git a/src/LanguageSuggestModal.ts b/src/LanguageSuggestModal.ts index 4358909..dd7a07b 100644 --- a/src/LanguageSuggestModal.ts +++ b/src/LanguageSuggestModal.ts @@ -1,6 +1,7 @@ import { App, SuggestModal } from "obsidian"; import { Language } from "./types"; import { TranslationService } from "./TranslationService"; +import { t } from "./i18n"; export class LanguageSuggestModal extends SuggestModal { private onSelect: (lang: Language) => void; @@ -17,8 +18,8 @@ export class LanguageSuggestModal extends SuggestModal { this.onSelect = onSelect; this.setPlaceholder( lastLanguage - ? `Last used: ${lastLanguage.name} — type to change…` - : "Type a language name (e.g. Spanish, German)…", + ? t("modalPlaceholderLastUsed", { name: lastLanguage.name }) + : t("modalPlaceholderDefault"), ); } @@ -40,10 +41,12 @@ export class LanguageSuggestModal extends SuggestModal { } renderSuggestion(lang: Language, el: HTMLElement): void { - el.createEl("span", { text: `${lang.name} — ${lang.code}` }); + el.createEl("span", { + text: t("modalSuggestion", { name: lang.name, code: lang.code }), + }); if (this.lastLanguage?.code === lang.code) { el.createEl("span", { - text: "Last used", + text: t("modalLastUsedBadge"), cls: "ai-translator-last-used-badge", }); } diff --git a/src/i18n/de.json b/src/i18n/de.json new file mode 100644 index 0000000..f5eed99 --- /dev/null +++ b/src/i18n/de.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Notiz übersetzen", + "commandTranslateNote": "Aktuelle Notiz übersetzen", + "commandTranslateToLast": "In die zuletzt verwendete Sprache übersetzen", + "menuTranslate": "Übersetzen...", + "menuTranslateToLast": "In die zuletzt verwendete Sprache übersetzen", + "settingHeading": "KI-Übersetzer", + "settingApiKeyName": "API-Schlüssel", + "settingApiKeyPlaceholder": "Fügen Sie hier Ihren API-Schlüssel ein", + "settingApiKeyDescPrefix": "Ihr l10n.dev API-Schlüssel. Erhalten Sie einen unter ", + "settingBalanceName": "Restguthaben", + "settingBalanceDesc": "Verbleibende Zeichen für die Übersetzung.", + "settingBalanceBuyBtn": "Zeichen kaufen", + "settingBalanceFree": " {{count}} Zeichen monatlich kostenlos.", + "settingBalanceLoading": " Lädt...", + "settingBalanceCount": " {{count}} Zeichen", + "settingOutputName": "Ausgabeverhalten", + "settingOutputDesc": "Wie übersetzte Inhalte gespeichert werden.", + "settingOutputNewNote": "Neue Notiz erstellen", + "settingOutputReplace": "Aktuellen Notizinhalt ersetzen", + "settingOutputAppend": "An aktuelle Notiz anhängen", + "settingFrontmatterName": "Frontmatter übersetzen", + "settingFrontmatterDesc": "Wenn aktiviert, wird das YAML-Frontmatter in die Übersetzung einbezogen.", + "settingGlossaryName": "Lexikon generieren & speichern", + "settingGlossaryDesc": "Wenn aktiviert, wird ein Lexikon basierend auf der Übersetzung erstellt, um die Konsistenz zwischen Notizen zu gewährleisten und für die zukünftige Verwendung gespeichert.", + "settingGlossaryManageLink": "Lexika verwalten.", + "settingLinguisticLink": "Sprachanweisungen verwalten.", + "noticeReadFailed": "Notiz konnte nicht gelesen werden.", + "noticeTranslating": "Übersetzung läuft...", + "noticeInvalidKey": "Ungültiger API-Schlüssel. Erhalten Sie Ihren Schlüssel unter {{url}}", + "noticeInsufficientBalance": "Guthaben nicht ausreichend. Laden Sie es unter {{url}} auf.", + "noticeTranslationFailed": "Übersetzung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "noticeTranslationComplete": "Übersetzung abgeschlossen. {{count}} Zeichen verwendet.", + "noticeRemainingBalance": " {{count}} Zeichen verbleibend.", + "noticeSaveFailed": "Übersetzte Notiz konnte nicht gespeichert werden.", + "noticeSetApiKey": "Bitte legen Sie Ihren l10n.dev API-Schlüssel in den Einstellungen fest.", + "noticeNoActiveNote": "Keine aktive Notiz zum Übersetzen.", + "modalPlaceholderLastUsed": "Zuletzt verwendet: {{name}} — zum Ändern tippen...", + "modalPlaceholderDefault": "Geben Sie einen Sprachennamen ein (z. B. Spanisch, Deutsch)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "Zuletzt verwendet" +} \ No newline at end of file diff --git a/src/i18n/en.json b/src/i18n/en.json new file mode 100644 index 0000000..89fd815 --- /dev/null +++ b/src/i18n/en.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Translate note", + "commandTranslateNote": "Translate current note", + "commandTranslateToLast": "Translate to last used language", + "menuTranslate": "Translate...", + "menuTranslateToLast": "Translate to last used language", + "settingHeading": "AI translator", + "settingApiKeyName": "API key", + "settingApiKeyPlaceholder": "Paste your API key here", + "settingApiKeyDescPrefix": "Your l10n.dev API key. Get one at ", + "settingBalanceName": "Remaining balance", + "settingBalanceDesc": "Characters remaining for translation.", + "settingBalanceBuyBtn": "Buy characters", + "settingBalanceFree": " {{count}} characters free monthly.", + "settingBalanceLoading": " Loading...", + "settingBalanceCount": " {{count}} characters", + "settingOutputName": "Output behavior", + "settingOutputDesc": "How translated content is saved.", + "settingOutputNewNote": "Create a new note", + "settingOutputReplace": "Replace current note content", + "settingOutputAppend": "Append to current note", + "settingFrontmatterName": "Translate frontmatter", + "settingFrontmatterDesc": "When enabled, YAML frontmatter will be included in the translation.", + "settingGlossaryName": "Generate & save glossary", + "settingGlossaryDesc": "When enabled, creates a glossary based on the translation to ensure consistency between notes and saves it for future use.", + "settingGlossaryManageLink": "Manage glossaries.", + "settingLinguisticLink": "Manage linguistic instructions.", + "noticeReadFailed": "Failed to read the note.", + "noticeTranslating": "Translating...", + "noticeInvalidKey": "Invalid API key. Get your key at {{url}}", + "noticeInsufficientBalance": "Insufficient balance. Top up at {{url}}", + "noticeTranslationFailed": "Translation failed. Please try again.", + "noticeTranslationComplete": "Translation complete. Used {{count}} characters.", + "noticeRemainingBalance": " {{count}} characters remaining.", + "noticeSaveFailed": "Failed to save the translated note.", + "noticeSetApiKey": "Please set your l10n.dev API key in settings.", + "noticeNoActiveNote": "No active note to translate.", + "modalPlaceholderLastUsed": "Last used: {{name}} \u2014 type to change...", + "modalPlaceholderDefault": "Type a language name (e.g. Spanish, German)...", + "modalSuggestion": "{{name}} \u2014 {{code}}", + "modalLastUsedBadge": "Last used" +} diff --git a/src/i18n/es.json b/src/i18n/es.json new file mode 100644 index 0000000..bfe2828 --- /dev/null +++ b/src/i18n/es.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Traducir nota", + "commandTranslateNote": "Traducir nota actual", + "commandTranslateToLast": "Traducir al último idioma utilizado", + "menuTranslate": "Traducir...", + "menuTranslateToLast": "Traducir al último idioma utilizado", + "settingHeading": "Traductor con IA", + "settingApiKeyName": "API key", + "settingApiKeyPlaceholder": "Pega tu API key aquí", + "settingApiKeyDescPrefix": "Tu API key de l10n.dev. Obtén una en ", + "settingBalanceName": "Saldo restante", + "settingBalanceDesc": "Caracteres restantes para la traducción.", + "settingBalanceBuyBtn": "Comprar caracteres", + "settingBalanceFree": " {{count}} caracteres gratuitos al mes.", + "settingBalanceLoading": " Cargando...", + "settingBalanceCount": " {{count}} caracteres", + "settingOutputName": "Comportamiento de salida", + "settingOutputDesc": "Cómo se guarda el contenido traducido.", + "settingOutputNewNote": "Crear una nueva nota", + "settingOutputReplace": "Reemplazar el contenido de la nota actual", + "settingOutputAppend": "Añadir a la nota actual", + "settingFrontmatterName": "Traducir frontmatter", + "settingFrontmatterDesc": "Cuando está activado, el frontmatter YAML se incluirá en la traducción.", + "settingGlossaryName": "Generar y guardar glosario", + "settingGlossaryDesc": "Cuando está activado, crea un glosario basado en la traducción para asegurar la consistencia de traducción entre notas y lo guarda para su uso futuro.", + "settingGlossaryManageLink": "Gestionar glosarios.", + "settingLinguisticLink": "Gestionar instrucciones lingüísticas.", + "noticeReadFailed": "Error al leer la nota.", + "noticeTranslating": "Traduciendo...", + "noticeInvalidKey": "API key no válida. Obtén tu clave en {{url}}", + "noticeInsufficientBalance": "Saldo insuficiente. Recarga en {{url}}", + "noticeTranslationFailed": "Error en la traducción. Por favor, inténtalo de nuevo.", + "noticeTranslationComplete": "Traducción completada. Se han utilizado {{count}} caracteres.", + "noticeRemainingBalance": " {{count}} caracteres restantes.", + "noticeSaveFailed": "Error al guardar la nota traducida.", + "noticeSetApiKey": "Por favor, configura tu API key de l10n.dev en los ajustes.", + "noticeNoActiveNote": "No hay ninguna nota activa para traducir.", + "modalPlaceholderLastUsed": "Último utilizado: {{name}} — escribe para cambiar...", + "modalPlaceholderDefault": "Escribe el nombre de un idioma (p. ej., español, alemán)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "Último utilizado" +} diff --git a/src/i18n/fr.json b/src/i18n/fr.json new file mode 100644 index 0000000..34cd30e --- /dev/null +++ b/src/i18n/fr.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Traduire la note", + "commandTranslateNote": "Traduire la note actuelle", + "commandTranslateToLast": "Traduire vers la dernière langue utilisée", + "menuTranslate": "Traduire...", + "menuTranslateToLast": "Traduire vers la dernière langue utilisée", + "settingHeading": "Traducteur IA", + "settingApiKeyName": "Clé API", + "settingApiKeyPlaceholder": "Collez votre clé API ici", + "settingApiKeyDescPrefix": "Votre clé API l10n.dev. Obtenez-en une sur ", + "settingBalanceName": "Solde restant", + "settingBalanceDesc": "Nombre de caractères restants pour la traduction.", + "settingBalanceBuyBtn": "Acheter des caractères", + "settingBalanceFree": " {{count}} caractères gratuits par mois.", + "settingBalanceLoading": " Chargement...", + "settingBalanceCount": " {{count}} caractères", + "settingOutputName": "Comportement de sortie", + "settingOutputDesc": "Comment le contenu traduit est enregistré.", + "settingOutputNewNote": "Créer une nouvelle note", + "settingOutputReplace": "Remplacer le contenu de la note actuelle", + "settingOutputAppend": "Ajouter à la note actuelle", + "settingFrontmatterName": "Traduire le frontmatter", + "settingFrontmatterDesc": "Si activé, le frontmatter YAML sera inclus dans la traduction.", + "settingGlossaryName": "Générer et enregistrer le glossaire", + "settingGlossaryDesc": "Si activé, crée un glossaire basé sur la traduction pour assurer la cohérence entre les notes et l'enregistre pour une utilisation future.", + "settingGlossaryManageLink": "Gérer les glossaires.", + "settingLinguisticLink": "Gérer les instructions linguistiques.", + "noticeReadFailed": "Échec de la lecture de la note.", + "noticeTranslating": "Traduction en cours...", + "noticeInvalidKey": "Clé API invalide. Obtenez votre clé sur {{url}}", + "noticeInsufficientBalance": "Solde insuffisant. Rechargez sur {{url}}", + "noticeTranslationFailed": "La traduction a échoué. Veuillez réessayer.", + "noticeTranslationComplete": "Traduction terminée. {{count}} caractères utilisés.", + "noticeRemainingBalance": " {{count}} caractères restants.", + "noticeSaveFailed": "Échec de l'enregistrement de la note traduite.", + "noticeSetApiKey": "Veuillez définir votre clé API l10n.dev dans les paramètres.", + "noticeNoActiveNote": "Aucune note active à traduire.", + "modalPlaceholderLastUsed": "Dernière utilisée : {{name}} — tapez pour changer...", + "modalPlaceholderDefault": "Tapez le nom d'une langue (ex. espagnol, allemand)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "Dernière utilisée" +} diff --git a/src/i18n/id.json b/src/i18n/id.json new file mode 100644 index 0000000..cb5defa --- /dev/null +++ b/src/i18n/id.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Terjemahkan catatan", + "commandTranslateNote": "Terjemahkan catatan saat ini", + "commandTranslateToLast": "Terjemahkan ke bahasa yang terakhir digunakan", + "menuTranslate": "Terjemahkan...", + "menuTranslateToLast": "Terjemahkan ke bahasa yang terakhir digunakan", + "settingHeading": "Penerjemah AI", + "settingApiKeyName": "API key", + "settingApiKeyPlaceholder": "Tempel API key Anda di sini", + "settingApiKeyDescPrefix": "API key l10n.dev Anda. Dapatkan di ", + "settingBalanceName": "Saldo tersisa", + "settingBalanceDesc": "Karakter tersisa untuk terjemahan.", + "settingBalanceBuyBtn": "Beli karakter", + "settingBalanceFree": " {{count}} karakter gratis setiap bulan.", + "settingBalanceLoading": " Memuat...", + "settingBalanceCount": " {{count}} karakter", + "settingOutputName": "Perilaku output", + "settingOutputDesc": "Cara konten terjemahan disimpan.", + "settingOutputNewNote": "Buat catatan baru", + "settingOutputReplace": "Ganti konten catatan saat ini", + "settingOutputAppend": "Tambahkan ke catatan saat ini", + "settingFrontmatterName": "Terjemahkan frontmatter", + "settingFrontmatterDesc": "Jika diaktifkan, YAML frontmatter akan disertakan dalam terjemahan.", + "settingGlossaryName": "Hasilkan & simpan glosarium", + "settingGlossaryDesc": "Jika diaktifkan, membuat glosarium berdasarkan terjemahan untuk memastikan konsistensi antar catatan dan menyimpannya untuk penggunaan di masa mendatang.", + "settingGlossaryManageLink": "Kelola glosarium.", + "settingLinguisticLink": "Kelola instruksi linguistik.", + "noticeReadFailed": "Gagal membaca catatan.", + "noticeTranslating": "Menerjemahkan...", + "noticeInvalidKey": "API key tidak valid. Dapatkan key Anda di {{url}}", + "noticeInsufficientBalance": "Saldo tidak mencukupi. Isi ulang di {{url}}", + "noticeTranslationFailed": "Terjemahan gagal. Silakan coba lagi.", + "noticeTranslationComplete": "Terjemahan selesai. Menggunakan {{count}} karakter.", + "noticeRemainingBalance": " {{count}} karakter tersisa.", + "noticeSaveFailed": "Gagal menyimpan catatan yang diterjemahkan.", + "noticeSetApiKey": "Harap atur API key l10n.dev Anda di pengaturan.", + "noticeNoActiveNote": "Tidak ada catatan aktif untuk diterjemahkan.", + "modalPlaceholderLastUsed": "Terakhir digunakan: {{name}} — ketik untuk mengubah...", + "modalPlaceholderDefault": "Ketik nama bahasa (misalnya Spanyol, Jerman)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "Terakhir digunakan" +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..15a35c1 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,160 @@ +import en from "./en.json"; +import de from "./de.json"; +import es from "./es.json"; +import fr from "./fr.json"; +import id from "./id.json"; +import it from "./it.json"; +import ja from "./ja.json"; +import ko from "./ko.json"; +import pt from "./pt.json"; +import zhCN from "./zh-CN.json"; +import zhTW from "./zh-TW.json"; + +/** Same keys as en.json, values widened to plain `string` for locale files. */ +export type Translations = { [K in keyof typeof en]: string }; + +// --------------------------------------------------------------------------- +// URL constants +// --------------------------------------------------------------------------- + +export const URLS = { + BASE: "https://l10n.dev", + API_KEYS: "https://l10n.dev/ws/keys", + PRICING: "https://l10n.dev/#pricing", + GLOSSARY: "https://l10n.dev/ws/translation-glossary", + LINGUISTIC: "https://l10n.dev/ws/linguistic-instructions", +} as const; + +// --------------------------------------------------------------------------- +// Locale normalization +// --------------------------------------------------------------------------- + +/** + * Maps moment.js locale codes (lowercase, hyphen-separated) to the plugin's + * supported locale codes. Falls back to "en" for anything unsupported. + */ +const LOCALE_MAP: Record = { + "zh-cn": "zh-CN", + "zh-tw": "zh-TW", + "zh-hk": "zh-TW", + "zh-mo": "zh-TW", + "pt-br": "pt", + "pt-pt": "pt", +}; + +const SUPPORTED_LOCALES = new Set([ + "en", + "de", + "es", + "fr", + "id", + "it", + "ja", + "ko", + "pt", + "zh-CN", + "zh-TW", +]); + +/** + * Resolves an arbitrary locale string to one of the plugin's supported codes, + * or "en" if no match is found. + */ +export function normalizeLocale(locale: string): string { + const lower = locale.toLowerCase(); + + // Explicit overrides (e.g. zh-cn → zh-CN) + if (LOCALE_MAP[lower]) return LOCALE_MAP[lower]; + + // Exact match (case-insensitive) + for (const supported of SUPPORTED_LOCALES) { + if (supported.toLowerCase() === lower) return supported; + } + + // Language-prefix fallback (e.g. "es-mx" → "es") + const prefix = lower.split(/[-_]/)[0] ?? lower; + for (const supported of SUPPORTED_LOCALES) { + if (supported.toLowerCase() === prefix) return supported; + } + + return "en"; +} + +// --------------------------------------------------------------------------- +// Locale detection +// --------------------------------------------------------------------------- + +/** Returns the current Obsidian UI locale code (via the bundled moment.js). */ +export function getLocale(): string { + const momentLocale = ( + window as typeof window & { moment?: { locale(): string } } + ).moment?.locale(); + return momentLocale ?? "en"; +} + +// --------------------------------------------------------------------------- +// Translation registry +// --------------------------------------------------------------------------- + +const locales: Record> = { + en, + de, + es, + fr, + id, + it, + ja, + ko, + pt, + "zh-CN": zhCN, + "zh-TW": zhTW, +}; + +// --------------------------------------------------------------------------- +// t() – look up a translation string with optional variable interpolation +// --------------------------------------------------------------------------- + +/** + * Returns the translated string for the given key in the current Obsidian + * locale, falling back to English when a translation is missing. + * + * Variable placeholders use the `{{key}}` syntax, e.g.: + * t("noticeTranslationComplete", { count: "1,234" }) + */ +export function t( + key: keyof Translations, + vars?: Record, +): string { + const locale = normalizeLocale(getLocale()); + const dict = locales[locale] ?? {}; + let str = dict[key] ?? en[key]; + + if (vars) { + for (const [k, v] of Object.entries(vars)) { + str = str.replace(`{{${k}}}`, String(v)); + } + } + + return str; +} + +// --------------------------------------------------------------------------- +// localizeUrl() – add a locale prefix to l10n.dev URLs +// --------------------------------------------------------------------------- + +/** + * Adds a locale prefix to l10n.dev URLs for non-English locales. + * Unsupported codes fall back to English (no prefix). + * Other domains (e.g. GitHub) are returned unchanged. + */ +export function localizeUrl(url: string, locale: string): string { + if (!url.startsWith(URLS.BASE)) { + return url; + } + const resolved = normalizeLocale(locale); + if (resolved === "en") { + return url; + } + const path = url.slice(URLS.BASE.length); // e.g. "/ws/keys" or "/#pricing" + return `${URLS.BASE}/${resolved.toLowerCase()}${path}`; +} diff --git a/src/i18n/it.json b/src/i18n/it.json new file mode 100644 index 0000000..c52ea8a --- /dev/null +++ b/src/i18n/it.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Traduci nota", + "commandTranslateNote": "Traduci nota corrente", + "commandTranslateToLast": "Traduci nell'ultima lingua utilizzata", + "menuTranslate": "Traduci...", + "menuTranslateToLast": "Traduci nell'ultima lingua utilizzata", + "settingHeading": "Traduttore IA", + "settingApiKeyName": "Chiave API", + "settingApiKeyPlaceholder": "Incolla qui la tua chiave API", + "settingApiKeyDescPrefix": "La tua chiave API l10n.dev. Ottienine una su ", + "settingBalanceName": "Saldo rimanente", + "settingBalanceDesc": "Caratteri rimanenti per la traduzione.", + "settingBalanceBuyBtn": "Acquista caratteri", + "settingBalanceFree": " {{count}} caratteri gratuiti mensili.", + "settingBalanceLoading": " Caricamento...", + "settingBalanceCount": " {{count}} caratteri", + "settingOutputName": "Comportamento output", + "settingOutputDesc": "Come viene salvato il contenuto tradotto.", + "settingOutputNewNote": "Crea una nuova nota", + "settingOutputReplace": "Sostituisci il contenuto della nota corrente", + "settingOutputAppend": "Accoda alla nota corrente", + "settingFrontmatterName": "Traduci front matter YAML", + "settingFrontmatterDesc": "Se abilitato, il front matter YAML sarà incluso nella traduzione.", + "settingGlossaryName": "Genera e salva glossario", + "settingGlossaryDesc": "Se abilitato, crea un glossario basato sulla traduzione per garantire la coerenza tra le note e lo salva per un uso futuro.", + "settingGlossaryManageLink": "Gestisci glossari.", + "settingLinguisticLink": "Gestisci istruzioni linguistiche.", + "noticeReadFailed": "Lettura della nota non riuscita.", + "noticeTranslating": "Traduzione in corso...", + "noticeInvalidKey": "Chiave API non valida. Ottieni la tua chiave su {{url}}", + "noticeInsufficientBalance": "Saldo insufficiente. Ricarica su {{url}}", + "noticeTranslationFailed": "Traduzione non riuscita. Riprova.", + "noticeTranslationComplete": "Traduzione completata. Utilizzati {{count}} caratteri.", + "noticeRemainingBalance": " {{count}} caratteri rimanenti.", + "noticeSaveFailed": "Salvataggio della nota tradotta non riuscito.", + "noticeSetApiKey": "Imposta la tua chiave API l10n.dev nelle impostazioni.", + "noticeNoActiveNote": "Nessuna nota attiva da tradurre.", + "modalPlaceholderLastUsed": "Ultima utilizzata: {{name}} — digita per cambiare...", + "modalPlaceholderDefault": "Digita il nome di una lingua (es. spagnolo, tedesco)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "Ultima utilizzata" +} diff --git a/src/i18n/ja.json b/src/i18n/ja.json new file mode 100644 index 0000000..e53f33b --- /dev/null +++ b/src/i18n/ja.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "ノートを翻訳", + "commandTranslateNote": "現在のノートを翻訳", + "commandTranslateToLast": "最後に使用した言語に翻訳", + "menuTranslate": "翻訳...", + "menuTranslateToLast": "最後に使用した言語に翻訳", + "settingHeading": "AI翻訳", + "settingApiKeyName": "APIキー", + "settingApiKeyPlaceholder": "ここにAPIキーを貼り付けてください", + "settingApiKeyDescPrefix": "l10n.devのAPIキー。取得はこちら: ", + "settingBalanceName": "残高", + "settingBalanceDesc": "翻訳可能な残りの文字数。", + "settingBalanceBuyBtn": "文字数を購入", + "settingBalanceFree": " 毎月{{count}}文字まで無料。", + "settingBalanceLoading": " 読み込み中...", + "settingBalanceCount": " {{count}}文字", + "settingOutputName": "出力動作", + "settingOutputDesc": "翻訳されたコンテンツの保存方法。", + "settingOutputNewNote": "新しいノートを作成", + "settingOutputReplace": "現在のノートの内容を置き換え", + "settingOutputAppend": "現在のノートに追記", + "settingFrontmatterName": "フロントマターを翻訳", + "settingFrontmatterDesc": "有効にすると、YAMLフロントマターが翻訳対象に含まれます。", + "settingGlossaryName": "用語集を生成・保存", + "settingGlossaryDesc": "有効にすると、翻訳に基づいて用語集を作成し、ノート間の一貫性を確保するために将来の使用に向けて保存します。", + "settingGlossaryManageLink": "用語集を管理。", + "settingLinguisticLink": "言語指示を管理。", + "noticeReadFailed": "ノートの読み込みに失敗しました。", + "noticeTranslating": "翻訳中...", + "noticeInvalidKey": "無効なAPIキーです。キーの取得はこちら: {{url}}", + "noticeInsufficientBalance": "残高不足です。チャージはこちら: {{url}}", + "noticeTranslationFailed": "翻訳に失敗しました。もう一度お試しください。", + "noticeTranslationComplete": "翻訳が完了しました。{{count}}文字使用しました。", + "noticeRemainingBalance": " 残り{{count}}文字です。", + "noticeSaveFailed": "翻訳されたノートの保存に失敗しました。", + "noticeSetApiKey": "設定でl10n.devのAPIキーを設定してください。", + "noticeNoActiveNote": "翻訳するアクティブなノートがありません。", + "modalPlaceholderLastUsed": "最後に使用: {{name}} — 入力して変更...", + "modalPlaceholderDefault": "言語名を入力してください (例: スペイン語、ドイツ語)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "最後に使用" +} diff --git a/src/i18n/ko.json b/src/i18n/ko.json new file mode 100644 index 0000000..2f199d5 --- /dev/null +++ b/src/i18n/ko.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "노트 번역", + "commandTranslateNote": "현재 노트 번역", + "commandTranslateToLast": "마지막으로 사용한 언어로 번역", + "menuTranslate": "번역...", + "menuTranslateToLast": "마지막으로 사용한 언어로 번역", + "settingHeading": "AI 번역기", + "settingApiKeyName": "API 키", + "settingApiKeyPlaceholder": "여기에 API 키를 붙여넣으세요", + "settingApiKeyDescPrefix": "l10n.dev API 키입니다. 다음에서 발급받으세요: ", + "settingBalanceName": "잔액", + "settingBalanceDesc": "번역에 사용할 수 있는 남은 문자 수입니다.", + "settingBalanceBuyBtn": "문자 구매", + "settingBalanceFree": " 매월 {{count}} 문자 무료.", + "settingBalanceLoading": " 로딩 중...", + "settingBalanceCount": " {{count}} 문자", + "settingOutputName": "출력 방식", + "settingOutputDesc": "번역된 콘텐츠가 저장되는 방식입니다.", + "settingOutputNewNote": "새 노트 생성", + "settingOutputReplace": "현재 노트 내용 교체", + "settingOutputAppend": "현재 노트에 추가", + "settingFrontmatterName": "프런트매터 번역", + "settingFrontmatterDesc": "활성화 시, YAML 프런트매터가 번역에 포함됩니다.", + "settingGlossaryName": "용어집 생성 및 저장", + "settingGlossaryDesc": "활성화 시, 노트 간 일관성을 유지하기 위해 번역을 기반으로 용어집을 생성하고 향후 사용을 위해 저장합니다.", + "settingGlossaryManageLink": "용어집 관리.", + "settingLinguisticLink": "언어 지침 관리.", + "noticeReadFailed": "노트를 읽지 못했습니다.", + "noticeTranslating": "번역 중...", + "noticeInvalidKey": "유효하지 않은 API 키입니다. 다음에서 키를 발급받으세요: {{url}}", + "noticeInsufficientBalance": "잔액이 부족합니다. 다음에서 충전하세요: {{url}}", + "noticeTranslationFailed": "번역에 실패했습니다. 다시 시도해 주세요.", + "noticeTranslationComplete": "번역이 완료되었습니다. {{count}} 문자가 사용되었습니다.", + "noticeRemainingBalance": " {{count}} 문자 남음.", + "noticeSaveFailed": "번역된 노트를 저장하지 못했습니다.", + "noticeSetApiKey": "설정에서 l10n.dev API 키를 설정해 주세요.", + "noticeNoActiveNote": "번역할 활성화된 노트가 없습니다.", + "modalPlaceholderLastUsed": "마지막 사용: {{name}} — 변경하려면 입력하세요...", + "modalPlaceholderDefault": "언어 이름을 입력하세요(예: 스페인어, 독일어)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "마지막 사용" +} diff --git a/src/i18n/pt.json b/src/i18n/pt.json new file mode 100644 index 0000000..e318568 --- /dev/null +++ b/src/i18n/pt.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "Traduzir nota", + "commandTranslateNote": "Traduzir nota atual", + "commandTranslateToLast": "Traduzir para o último idioma usado", + "menuTranslate": "Traduzir...", + "menuTranslateToLast": "Traduzir para o último idioma usado", + "settingHeading": "Tradutor por IA", + "settingApiKeyName": "Chave de API", + "settingApiKeyPlaceholder": "Cole sua chave de API aqui", + "settingApiKeyDescPrefix": "Sua chave de API l10n.dev. Obtenha uma em ", + "settingBalanceName": "Saldo restante", + "settingBalanceDesc": "Caracteres restantes para tradução.", + "settingBalanceBuyBtn": "Comprar caracteres", + "settingBalanceFree": " {{count}} caracteres gratuitos mensalmente.", + "settingBalanceLoading": " Carregando...", + "settingBalanceCount": " {{count}} caracteres", + "settingOutputName": "Comportamento de saída", + "settingOutputDesc": "Como o conteúdo traduzido é salvo.", + "settingOutputNewNote": "Criar uma nova nota", + "settingOutputReplace": "Substituir o conteúdo da nota atual", + "settingOutputAppend": "Adicionar à nota atual", + "settingFrontmatterName": "Traduzir frontmatter", + "settingFrontmatterDesc": "Quando ativado, o frontmatter YAML será incluído na tradução.", + "settingGlossaryName": "Gerar e salvar glossário", + "settingGlossaryDesc": "Quando ativado, cria um glossário baseado na tradução para garantir a consistência entre notas e o salva para uso futuro.", + "settingGlossaryManageLink": "Gerenciar glossários.", + "settingLinguisticLink": "Gerenciar instruções linguísticas.", + "noticeReadFailed": "Falha ao ler a nota.", + "noticeTranslating": "Traduzindo...", + "noticeInvalidKey": "Chave de API inválida. Obtenha sua chave em {{url}}", + "noticeInsufficientBalance": "Saldo insuficiente. Recarregue em {{url}}", + "noticeTranslationFailed": "Falha na tradução. Tente novamente.", + "noticeTranslationComplete": "Tradução concluída. {{count}} caracteres usados.", + "noticeRemainingBalance": " {{count}} caracteres restantes.", + "noticeSaveFailed": "Falha ao salvar a nota traduzida.", + "noticeSetApiKey": "Por favor, defina sua chave de API l10n.dev nas configurações.", + "noticeNoActiveNote": "Nenhuma nota ativa para traduzir.", + "modalPlaceholderLastUsed": "Último usado: {{name}} — digite para alterar...", + "modalPlaceholderDefault": "Digite o nome de um idioma (ex.: espanhol, alemão)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "Último usado" +} diff --git a/src/i18n/zh-CN.json b/src/i18n/zh-CN.json new file mode 100644 index 0000000..38c7b49 --- /dev/null +++ b/src/i18n/zh-CN.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "翻译笔记", + "commandTranslateNote": "翻译当前笔记", + "commandTranslateToLast": "翻译为上次使用的语言", + "menuTranslate": "翻译...", + "menuTranslateToLast": "翻译为上次使用的语言", + "settingHeading": "AI翻译器", + "settingApiKeyName": "API密钥", + "settingApiKeyPlaceholder": "在此处粘贴您的API密钥", + "settingApiKeyDescPrefix": "您的l10n.dev API密钥。获取地址:", + "settingBalanceName": "剩余余额", + "settingBalanceDesc": "剩余可翻译字符数。", + "settingBalanceBuyBtn": "购买字符", + "settingBalanceFree": " 每月免费 {{count}} 个字符。", + "settingBalanceLoading": " 加载中...", + "settingBalanceCount": "{{count}} 个字符", + "settingOutputName": "输出行为", + "settingOutputDesc": "翻译内容的保存方式。", + "settingOutputNewNote": "创建新笔记", + "settingOutputReplace": "替换当前笔记内容", + "settingOutputAppend": "追加到当前笔记", + "settingFrontmatterName": "翻译Frontmatter", + "settingFrontmatterDesc": "启用后,YAML Frontmatter将包含在翻译中。", + "settingGlossaryName": "生成并保存词汇表", + "settingGlossaryDesc": "启用后,根据翻译内容创建词汇表以确保笔记间的一致性,并保存以供将来使用。", + "settingGlossaryManageLink": "管理词汇表。", + "settingLinguisticLink": "管理语言指令。", + "noticeReadFailed": "读取笔记失败。", + "noticeTranslating": "正在翻译...", + "noticeInvalidKey": "API密钥无效。请在 {{url}} 获取您的密钥", + "noticeInsufficientBalance": "余额不足。请在 {{url}} 充值", + "noticeTranslationFailed": "翻译失败。请重试。", + "noticeTranslationComplete": "翻译完成。已使用 {{count}} 个字符。", + "noticeRemainingBalance": " 剩余 {{count}} 个字符。", + "noticeSaveFailed": "保存翻译后的笔记失败。", + "noticeSetApiKey": "请在设置中配置您的l10n.dev API密钥。", + "noticeNoActiveNote": "没有可翻译的活动笔记。", + "modalPlaceholderLastUsed": "上次使用:{{name}} — 输入以更改...", + "modalPlaceholderDefault": "输入语言名称(例如:西班牙语、德语)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "上次使用" +} \ No newline at end of file diff --git a/src/i18n/zh-TW.json b/src/i18n/zh-TW.json new file mode 100644 index 0000000..54dd752 --- /dev/null +++ b/src/i18n/zh-TW.json @@ -0,0 +1,42 @@ +{ + "ribbonTooltip": "翻譯筆記", + "commandTranslateNote": "翻譯目前筆記", + "commandTranslateToLast": "翻譯為上次使用的語言", + "menuTranslate": "翻譯...", + "menuTranslateToLast": "翻譯為上次使用的語言", + "settingHeading": "AI 翻譯", + "settingApiKeyName": "API 金鑰", + "settingApiKeyPlaceholder": "請在此貼上您的 API 金鑰", + "settingApiKeyDescPrefix": "您的 l10n.dev API 金鑰。請至此處取得:", + "settingBalanceName": "剩餘額度", + "settingBalanceDesc": "翻譯剩餘字元數。", + "settingBalanceBuyBtn": "購買字元", + "settingBalanceFree": "每月免費 {{count}} 字元。", + "settingBalanceLoading": "載入中...", + "settingBalanceCount": "{{count}} 字元", + "settingOutputName": "輸出行為", + "settingOutputDesc": "翻譯內容的儲存方式。", + "settingOutputNewNote": "建立新筆記", + "settingOutputReplace": "取代目前筆記內容", + "settingOutputAppend": "附加至目前筆記", + "settingFrontmatterName": "翻譯 frontmatter", + "settingFrontmatterDesc": "啟用後,YAML frontmatter 將包含在翻譯中。", + "settingGlossaryName": "產生並儲存詞彙表", + "settingGlossaryDesc": "啟用後,將根據翻譯內容建立詞彙表,以確保筆記間的一致性,並儲存供日後使用。", + "settingGlossaryManageLink": "管理詞彙表。", + "settingLinguisticLink": "管理語言指令。", + "noticeReadFailed": "讀取筆記失敗。", + "noticeTranslating": "翻譯中...", + "noticeInvalidKey": "API 金鑰無效。請至 {{url}} 取得您的金鑰", + "noticeInsufficientBalance": "餘額不足。請至 {{url}} 加值", + "noticeTranslationFailed": "翻譯失敗。請再試一次。", + "noticeTranslationComplete": "翻譯完成。已使用 {{count}} 字元。", + "noticeRemainingBalance": "剩餘 {{count}} 字元。", + "noticeSaveFailed": "儲存翻譯後的筆記失敗。", + "noticeSetApiKey": "請在設定中設定您的 l10n.dev API 金鑰。", + "noticeNoActiveNote": "沒有可翻譯的有效筆記。", + "modalPlaceholderLastUsed": "上次使用:{{name}} — 輸入以變更...", + "modalPlaceholderDefault": "輸入語言名稱(例如:西班牙語、德語)...", + "modalSuggestion": "{{name}} — {{code}}", + "modalLastUsedBadge": "上次使用" +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 56d5d14..9153781 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import { L10nSettings } from "./types"; import { DEFAULT_SETTINGS, L10nSettingsTab } from "./settings"; import { translateActiveNote, translateToLastLanguage } from "./translator"; +import { t } from "./i18n"; export default class L10nPlugin extends Plugin { settings: L10nSettings = { ...DEFAULT_SETTINGS }; @@ -9,13 +10,13 @@ export default class L10nPlugin extends Plugin { async onload() { await this.loadSettings(); - this.addRibbonIcon("languages", "Translate note", () => { + this.addRibbonIcon("languages", t("ribbonTooltip"), () => { translateActiveNote(this); }); this.addCommand({ id: "translate-current-note", - name: "Translate current note", + name: t("commandTranslateNote"), callback: () => { translateActiveNote(this); }, @@ -23,7 +24,7 @@ export default class L10nPlugin extends Plugin { this.addCommand({ id: "translate-to-last-language", - name: "Translate to last used language", + name: t("commandTranslateToLast"), callback: () => { translateToLastLanguage(this); }, @@ -35,7 +36,7 @@ export default class L10nPlugin extends Plugin { (menu: Menu, abstractFile: TAbstractFile) => { if (!(abstractFile instanceof TFile)) return; menu.addItem((item) => { - item.setTitle("Translate…") + item.setTitle(t("menuTranslate")) .setIcon("languages") .onClick(() => { translateActiveNote(this, abstractFile); @@ -51,7 +52,7 @@ export default class L10nPlugin extends Plugin { (menu: Menu, abstractFile: TAbstractFile) => { if (!(abstractFile instanceof TFile)) return; menu.addItem((item) => { - item.setTitle("Translate to last used language") + item.setTitle(t("menuTranslateToLast")) .setIcon("languages") .onClick(() => { translateToLastLanguage(this, abstractFile); @@ -64,7 +65,7 @@ export default class L10nPlugin extends Plugin { this.registerEvent( this.app.workspace.on("editor-menu", (menu: Menu) => { menu.addItem((item) => { - item.setTitle("Translate to last used language") + item.setTitle(t("menuTranslateToLast")) .setIcon("languages") .onClick(() => { translateToLastLanguage(this); @@ -76,7 +77,7 @@ export default class L10nPlugin extends Plugin { this.registerEvent( this.app.workspace.on("editor-menu", (menu: Menu) => { menu.addItem((item) => { - item.setTitle("Translate…") + item.setTitle(t("menuTranslate")) .setIcon("languages") .onClick(() => { translateActiveNote(this); diff --git a/src/settings.ts b/src/settings.ts index fccf350..1bebbac 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,6 +1,7 @@ import { App, Plugin, PluginSettingTab, Setting } from "obsidian"; import { L10nSettings, OutputBehavior, PluginWithSettings } from "./types"; import { TranslationService } from "./TranslationService"; +import { t, localizeUrl, getLocale, URLS } from "./i18n"; export type { L10nSettings }; @@ -23,36 +24,42 @@ export class L10nSettingsTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - new Setting(containerEl).setName("AI translator").setHeading(); + new Setting(containerEl).setName(t("settingHeading")).setHeading(); // Balance refresh helper (defined before API key so onChange can call it) const refreshBalance = (apiKey: string) => { if (!apiKey) { quotaEl.setText( - ` ${(10000).toLocaleString()} characters free monthly.`, + t("settingBalanceFree", { + count: (10000).toLocaleString(), + }), ); return; } - quotaEl.setText(" Loading…"); + quotaEl.setText(t("settingBalanceLoading")); TranslationService.getBalance(apiKey) .then((res) => { quotaEl.setText( - ` ${res.currentBalance.toLocaleString()} characters`, + t("settingBalanceCount", { + count: res.currentBalance.toLocaleString(), + }), ); }) .catch(() => { quotaEl.setText( - ` ${(10000).toLocaleString()} characters free monthly.`, + t("settingBalanceFree", { + count: (10000).toLocaleString(), + }), ); }); }; // API Key const apiKeySetting = new Setting(containerEl) - .setName("API key") + .setName(t("settingApiKeyName")) .addText((text) => { text.inputEl.type = "password"; - text.setPlaceholder("Paste your API key here") + text.setPlaceholder(t("settingApiKeyPlaceholder")) .setValue(this.plugin.settings.apiKey) .onChange(async (value) => { this.plugin.settings.apiKey = value.trim(); @@ -60,20 +67,26 @@ export class L10nSettingsTab extends PluginSettingTab { refreshBalance(this.plugin.settings.apiKey); }); }); - apiKeySetting.descEl.appendText("Your l10n.dev API key. Get one at "); + apiKeySetting.descEl.appendText(t("settingApiKeyDescPrefix")); apiKeySetting.descEl.createEl("a", { - text: "l10n.dev/ws/keys", - href: "https://l10n.dev/ws/keys", + text: localizeUrl(URLS.API_KEYS, getLocale()).replace( + /^https?:\/\//, + "", + ), + href: localizeUrl(URLS.API_KEYS, getLocale()), attr: { target: "_blank", rel: "noopener noreferrer" }, }); // Quota display const quotaSetting = new Setting(containerEl) - .setName("Remaining balance") - .setDesc("Characters remaining for translation.") + .setName(t("settingBalanceName")) + .setDesc(t("settingBalanceDesc")) .addButton((btn) => { - btn.setButtonText("Buy characters").onClick(() => { - window.open("https://l10n.dev/#pricing", "_blank"); + btn.setButtonText(t("settingBalanceBuyBtn")).onClick(() => { + window.open( + localizeUrl(URLS.PRICING, getLocale()), + "_blank", + ); }); }); const quotaEl = quotaSetting.descEl.createEl("span", { text: "" }); @@ -83,13 +96,13 @@ export class L10nSettingsTab extends PluginSettingTab { // Output behavior new Setting(containerEl) - .setName("Output behavior") - .setDesc("How translated content is saved.") + .setName(t("settingOutputName")) + .setDesc(t("settingOutputDesc")) .addDropdown((dropdown) => { dropdown - .addOption("new-note", "Create a new note") - .addOption("replace", "Replace current note content") - .addOption("append", "Append to current note") + .addOption("new-note", t("settingOutputNewNote")) + .addOption("replace", t("settingOutputReplace")) + .addOption("append", t("settingOutputAppend")) .setValue(this.plugin.settings.outputBehavior) .onChange(async (value) => { this.plugin.settings.outputBehavior = @@ -100,10 +113,8 @@ export class L10nSettingsTab extends PluginSettingTab { // Translate frontmatter new Setting(containerEl) - .setName("Translate frontmatter") - .setDesc( - "When enabled, YAML frontmatter will be included in the translation.", - ) + .setName(t("settingFrontmatterName")) + .setDesc(t("settingFrontmatterDesc")) .addToggle((toggle) => { toggle .setValue(this.plugin.settings.translateFrontmatter) @@ -115,10 +126,8 @@ export class L10nSettingsTab extends PluginSettingTab { // Generate glossary const glossarySetting = new Setting(containerEl) - .setName("Generate & save glossary") - .setDesc( - "When enabled, creates a glossary based on the translation to ensure consistency between notes and saves it for future use. ", - ) + .setName(t("settingGlossaryName")) + .setDesc(t("settingGlossaryDesc")) .addToggle((toggle) => { toggle .setValue(this.plugin.settings.generateGlossary) @@ -129,9 +138,16 @@ export class L10nSettingsTab extends PluginSettingTab { }); glossarySetting.descEl.createEl("a", { - text: "Manage glossaries", - href: "https://l10n.dev/ws/translation-glossary", + text: t("settingGlossaryManageLink"), + href: localizeUrl(URLS.GLOSSARY, getLocale()), attr: { target: "_blank", rel: "noopener noreferrer" }, + cls: "inline-link", + }); + glossarySetting.descEl.createEl("a", { + text: t("settingLinguisticLink"), + href: localizeUrl(URLS.LINGUISTIC, getLocale()), + attr: { target: "_blank", rel: "noopener noreferrer" }, + cls: "inline-link", }); } } diff --git a/src/translator.ts b/src/translator.ts index 76c7408..48b02ea 100644 --- a/src/translator.ts +++ b/src/translator.ts @@ -2,6 +2,7 @@ import { Notice, TFile } from "obsidian"; import { LanguageSuggestModal } from "./LanguageSuggestModal"; import { TranslationService } from "./TranslationService"; import { Language, PluginWithSettings } from "./types"; +import { t, localizeUrl, getLocale, URLS } from "./i18n"; const FRONTMATTER_REGEX = /^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/; @@ -22,7 +23,7 @@ async function doTranslate( try { content = await plugin.app.vault.read(file); } catch { - new Notice("Failed to read the note."); + new Notice(t("noticeReadFailed")); return; } @@ -39,7 +40,7 @@ async function doTranslate( let translated: string; let charsUsed = 0; let remainingBalance: number | null | undefined; - const progressNotice = new Notice("Translating…", 0); + const progressNotice = new Notice(t("noticeTranslating"), 0); try { const result = await TranslationService.translate( plugin.settings.apiKey, @@ -61,14 +62,18 @@ async function doTranslate( const msg = err instanceof Error ? err.message : ""; if (msg === "unauthorized") { new Notice( - "Invalid API key. Get your key at https://l10n.dev/ws/keys", + t("noticeInvalidKey", { + url: localizeUrl(URLS.API_KEYS, getLocale()), + }), ); } else if (msg === "quota_exceeded") { new Notice( - "Insufficient balance. Top up at https://l10n.dev/#pricing", + t("noticeInsufficientBalance", { + url: localizeUrl(URLS.PRICING, getLocale()), + }), ); } else { - new Notice("Translation failed. Please try again."); + new Notice(t("noticeTranslationFailed")); } return; } @@ -100,13 +105,17 @@ async function doTranslate( } const balanceText = remainingBalance != null - ? ` ${remainingBalance.toLocaleString()} characters remaining.` + ? t("noticeRemainingBalance", { + count: remainingBalance.toLocaleString(), + }) : ""; new Notice( - `Translation complete. Used ${charsUsed.toLocaleString()} characters.${balanceText}`, + t("noticeTranslationComplete", { + count: charsUsed.toLocaleString(), + }) + balanceText, ); } catch { - new Notice("Failed to save the translated note."); + new Notice(t("noticeSaveFailed")); } } @@ -115,13 +124,13 @@ export function translateActiveNote( targetFile?: TFile, ): void { if (!plugin.settings.apiKey) { - new Notice("Please set your l10n.dev API key in settings."); + new Notice(t("noticeSetApiKey")); return; } const file = targetFile ?? plugin.app.workspace.getActiveFile(); if (!file) { - new Notice("No active note to translate."); + new Notice(t("noticeNoActiveNote")); return; } @@ -146,13 +155,13 @@ export function translateToLastLanguage( } if (!plugin.settings.apiKey) { - new Notice("Please set your l10n.dev API key in settings."); + new Notice(t("noticeSetApiKey")); return; } const file = targetFile ?? plugin.app.workspace.getActiveFile(); if (!file) { - new Notice("No active note to translate."); + new Notice(t("noticeNoActiveNote")); return; } diff --git a/styles.css b/styles.css index 8202cbd..659c36a 100644 --- a/styles.css +++ b/styles.css @@ -16,3 +16,7 @@ If your plugin does not need CSS, delete this file. color: var(--text-on-accent); vertical-align: middle; } + +.inline-link { + margin-left: 8px; +} diff --git a/tsconfig.json b/tsconfig.json index a4e09c8..51d380c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,10 +13,11 @@ "noUncheckedIndexedAccess": true, "isolatedModules": true, "strictNullChecks": true, + "resolveJsonModule": true, "strictBindCallApply": true, "allowSyntheticDefaultImports": true, "useUnknownInCatchVariables": true, - "lib": ["DOM", "ES5", "ES6", "ES7"] + "lib": ["DOM", "ES2017"] }, "include": ["src/**/*.ts"] } diff --git a/versions.json b/versions.json index fad09e0..8f5c910 100644 --- a/versions.json +++ b/versions.json @@ -6,5 +6,6 @@ "1.0.4": "0.15.0", "1.1.0": "0.15.0", "1.1.1": "1.1.7", - "1.1.2": "1.1.7" + "1.1.2": "1.1.7", + "1.2.0": "1.1.7" }