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.
This commit is contained in:
Anton Antonov 2026-06-24 19:28:51 +05:00 committed by GitHub
parent be889aa3df
commit 5bc9556485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 754 additions and 58 deletions

View file

@ -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

View file

@ -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.

View file

@ -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",

4
package-lock.json generated
View file

@ -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"

View file

@ -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",

View file

@ -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<Language> {
private onSelect: (lang: Language) => void;
@ -17,8 +18,8 @@ export class LanguageSuggestModal extends SuggestModal<Language> {
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<Language> {
}
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",
});
}

42
src/i18n/de.json Normal file
View file

@ -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"
}

42
src/i18n/en.json Normal file
View file

@ -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"
}

42
src/i18n/es.json Normal file
View file

@ -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"
}

42
src/i18n/fr.json Normal file
View file

@ -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"
}

42
src/i18n/id.json Normal file
View file

@ -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"
}

160
src/i18n/index.ts Normal file
View file

@ -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<string, string> = {
"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<string, Partial<Translations>> = {
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, string | number>,
): 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}`;
}

42
src/i18n/it.json Normal file
View file

@ -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"
}

42
src/i18n/ja.json Normal file
View file

@ -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": "最後に使用"
}

42
src/i18n/ko.json Normal file
View file

@ -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": "마지막 사용"
}

42
src/i18n/pt.json Normal file
View file

@ -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"
}

42
src/i18n/zh-CN.json Normal file
View file

@ -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": "上次使用"
}

42
src/i18n/zh-TW.json Normal file
View file

@ -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": "上次使用"
}

View file

@ -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);

View file

@ -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",
});
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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"]
}

View file

@ -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"
}