mirror of
https://github.com/l10n-dev/obsidian-plugin-ai-translator.git
synced 2026-07-22 06:56:27 +00:00
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:
parent
be889aa3df
commit
5bc9556485
24 changed files with 754 additions and 58 deletions
|
|
@ -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
|
||||
|
|
|
|||
30
README.md
30
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.
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
42
src/i18n/de.json
Normal 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
42
src/i18n/en.json
Normal 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
42
src/i18n/es.json
Normal 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
42
src/i18n/fr.json
Normal 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
42
src/i18n/id.json
Normal 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
160
src/i18n/index.ts
Normal 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
42
src/i18n/it.json
Normal 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
42
src/i18n/ja.json
Normal 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
42
src/i18n/ko.json
Normal 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
42
src/i18n/pt.json
Normal 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
42
src/i18n/zh-CN.json
Normal 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
42
src/i18n/zh-TW.json
Normal 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": "上次使用"
|
||||
}
|
||||
15
src/main.ts
15
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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue