diff --git a/.vscode/settings.json b/.vscode/settings.json index e4df34ba..4d01067b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,7 @@ "Callirhoe", "Callirrhoe", "Cerebras", + "colour", "compadd", "compdef", "compinit", @@ -129,6 +130,7 @@ "opencode", "opencontainers", "openrouter", + "organise", "Orus", "osascript", "otiai", diff --git a/cmd/generate_changelog/incoming/1778.txt b/cmd/generate_changelog/incoming/1778.txt new file mode 100644 index 00000000..d59eba80 --- /dev/null +++ b/cmd/generate_changelog/incoming/1778.txt @@ -0,0 +1,7 @@ +### PR [#1778](https://github.com/danielmiessler/Fabric/pull/1778) by [ksylvan](https://github.com/ksylvan): Add Portuguese Language Variants Support (pt-BR and pt-PT) + +- Add Brazilian Portuguese (pt-BR) translation file +- Add European Portuguese (pt-PT) translation file +- Implement BCP 47 locale normalization system +- Create fallback chain for language variants +- Add default variant mapping for Portuguese diff --git a/docs/i18n-variants.md b/docs/i18n-variants.md new file mode 100644 index 00000000..bf9b3165 --- /dev/null +++ b/docs/i18n-variants.md @@ -0,0 +1,140 @@ +# Language Variants Support in Fabric + +## Current Implementation + +As of this update, Fabric supports Portuguese language variants: + +- `pt-BR` - Brazilian Portuguese +- `pt-PT` - European Portuguese +- `pt` - defaults to `pt-BR` for backward compatibility + +## Architecture + +The i18n system supports language variants through: + +1. **BCP 47 Format**: All locales are normalized to BCP 47 format (language-REGION) +2. **Fallback Chain**: Regional variants fall back to base language, then to configured defaults +3. **Default Variant Mapping**: Languages without base files can specify default regional variants +4. **Flexible Input**: Accepts both underscore (pt_BR) and hyphen (pt-BR) formats + +## Recommended Future Variants + +Based on user demographics and linguistic differences, these variants would provide the most value: + +### High Priority + +1. **Chinese Variants** + - `zh-CN` - Simplified Chinese (Mainland China) + - `zh-TW` - Traditional Chinese (Taiwan) + - `zh-HK` - Traditional Chinese (Hong Kong) + - Default: `zh` → `zh-CN` + - Rationale: Significant script and vocabulary differences + +2. **Spanish Variants** + - `es-ES` - European Spanish (Spain) + - `es-MX` - Mexican Spanish + - `es-AR` - Argentinian Spanish + - Default: `es` → `es-ES` + - Rationale: Notable vocabulary and conjugation differences + +3. **English Variants** + - `en-US` - American English + - `en-GB` - British English + - `en-AU` - Australian English + - Default: `en` → `en-US` + - Rationale: Spelling differences (color/colour, organize/organise) + +4. **French Variants** + - `fr-FR` - France French + - `fr-CA` - Canadian French + - Default: `fr` → `fr-FR` + - Rationale: Some vocabulary and expression differences + +5. **Arabic Variants** + - `ar-SA` - Saudi Arabic (Modern Standard) + - `ar-EG` - Egyptian Arabic + - Default: `ar` → `ar-SA` + - Rationale: Significant dialectal differences + +6. **German Variants** + - `de-DE` - Germany German + - `de-AT` - Austrian German + - `de-CH` - Swiss German + - Default: `de` → `de-DE` + - Rationale: Minor differences, mostly vocabulary + +## Implementation Guidelines + +When adding new language variants: + +1. **Determine the Base**: Decide which variant should be the default +2. **Create Variant Files**: Copy base file and adjust for regional differences +3. **Update Default Map**: Add to `defaultLanguageVariants` if needed +4. **Focus on Key Differences**: + - Technical terminology + - Common UI terms (file/ficheiro, save/guardar) + - Date/time formats + - Currency references + - Formal/informal address conventions + +5. **Test Thoroughly**: Ensure fallback chain works correctly + +## Adding a New Variant + +To add a new language variant: + +1. Copy the base language file: + + ```bash + cp locales/es.json locales/es-MX.json + ``` + +2. Adjust translations for regional differences + +3. If this is the first variant for a language, update `i18n.go`: + + ```go + var defaultLanguageVariants = map[string]string{ + "pt": "pt-BR", + "es": "es-MX", // Add if Mexican Spanish should be default + } + ``` + +4. Add tests for the new variant + +5. Update documentation + +## Language Variant Naming Convention + +Follow BCP 47 standards: + +- Language code: lowercase (pt, es, en) +- Region code: uppercase (BR, PT, US) +- Separator: hyphen (pt-BR, not pt_BR) + +Input normalization handles various formats, but files and internal references should use BCP 47. + +## Testing Variants + +Test each variant with: + +```bash +# Direct specification +fabric --help -g=pt-BR +fabric --help -g=pt-PT + +# Environment variable +LANG=pt_BR.UTF-8 fabric --help + +# Fallback behavior +fabric --help -g=pt # Should use pt-BR +``` + +## Maintenance Considerations + +When updating translations: + +1. Update all variants of a language together +2. Ensure key parity across all variants +3. Test fallback behavior after changes +4. Consider using translation memory tools for consistency diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 2ac42ba4..d3ad466a 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -25,6 +25,22 @@ var ( initOnce sync.Once ) +// defaultLanguageVariants maps language codes without regions to their default regional variants. +// This is used when a language without a base file is requested. +var defaultLanguageVariants = map[string]string{ + "pt": "pt-BR", // Portuguese defaults to Brazilian Portuguese for backward compatibility + // Note: We currently have base files for these languages, but if we add regional variants + // in the future, these defaults will be used: + // "de": "de-DE", // German would default to Germany German + // "en": "en-US", // English would default to US English + // "es": "es-ES", // Spanish would default to Spain Spanish + // "fa": "fa-IR", // Persian would default to Iran Persian + // "fr": "fr-FR", // French would default to France French + // "it": "it-IT", // Italian would default to Italy Italian + // "ja": "ja-JP", // Japanese would default to Japan Japanese + // "zh": "zh-CN", // Chinese would default to Simplified Chinese +} + // Init initializes the i18n bundle and localizer. It loads the specified locale // and falls back to English if loading fails. // Translation files are searched in the user config directory and downloaded @@ -35,6 +51,8 @@ var ( func Init(locale string) (*i18n.Localizer, error) { // Use preferred locale detection if no explicit locale provided locale = getPreferredLocale(locale) + // Normalize the locale to BCP 47 format (with hyphens) + locale = normalizeToBCP47(locale) if locale == "" { locale = "en" } @@ -42,19 +60,21 @@ func Init(locale string) (*i18n.Localizer, error) { bundle := i18n.NewBundle(language.English) bundle.RegisterUnmarshalFunc("json", json.Unmarshal) - // load embedded translations for the requested locale if available + // Build a list of locale candidates to try + locales := getLocaleCandidates(locale) + + // Try to load embedded translations for each candidate embedded := false - if data, err := localeFS.ReadFile("locales/" + locale + ".json"); err == nil { - _, _ = bundle.ParseMessageFileBytes(data, locale+".json") - embedded = true - } else if strings.Contains(locale, "-") { - // Try base language if regional variant not found (e.g., es-ES -> es) - baseLang := strings.Split(locale, "-")[0] - if data, err := localeFS.ReadFile("locales/" + baseLang + ".json"); err == nil { - _, _ = bundle.ParseMessageFileBytes(data, baseLang+".json") + for _, candidate := range locales { + if data, err := localeFS.ReadFile("locales/" + candidate + ".json"); err == nil { + _, _ = bundle.ParseMessageFileBytes(data, candidate+".json") embedded = true + locale = candidate // Update locale to what was actually loaded + break } } + + // Fall back to English if nothing was loaded if !embedded { if data, err := localeFS.ReadFile("locales/en.json"); err == nil { _, _ = bundle.ParseMessageFileBytes(data, "en.json") @@ -158,3 +178,63 @@ func tryGetMessage(locale, messageID string) string { } return "" } + +// normalizeToBCP47 normalizes a locale string to BCP 47 format. +// Converts underscores to hyphens and ensures proper casing (language-REGION). +func normalizeToBCP47(locale string) string { + if locale == "" { + return "" + } + + // Replace underscores with hyphens + locale = strings.ReplaceAll(locale, "_", "-") + + // Split into parts + parts := strings.Split(locale, "-") + if len(parts) == 1 { + // Language only, lowercase it + return strings.ToLower(parts[0]) + } else if len(parts) >= 2 { + // Language and region (and possibly more) + // Lowercase language, uppercase region + parts[0] = strings.ToLower(parts[0]) + parts[1] = strings.ToUpper(parts[1]) + return strings.Join(parts[:2], "-") // Return only language-REGION + } + + return locale +} + +// getLocaleCandidates returns a list of locale candidates to try, in order of preference. +// For example, for "pt-PT" it returns ["pt-PT", "pt", "pt-BR"] (where pt-BR is the default for pt). +func getLocaleCandidates(locale string) []string { + candidates := []string{} + + if locale == "" { + return candidates + } + + // First candidate is always the requested locale + candidates = append(candidates, locale) + + // If it's a regional variant, add the base language as a candidate + if strings.Contains(locale, "-") { + baseLang := strings.Split(locale, "-")[0] + candidates = append(candidates, baseLang) + + // Also check if the base language has a default variant + if defaultVariant, exists := defaultLanguageVariants[baseLang]; exists { + // Only add if it's different from what we already have + if defaultVariant != locale { + candidates = append(candidates, defaultVariant) + } + } + } else { + // If this is a base language without a region, check for default variant + if defaultVariant, exists := defaultLanguageVariants[locale]; exists { + candidates = append(candidates, defaultVariant) + } + } + + return candidates +} diff --git a/internal/i18n/i18n_variants_test.go b/internal/i18n/i18n_variants_test.go new file mode 100644 index 00000000..64ff2c13 --- /dev/null +++ b/internal/i18n/i18n_variants_test.go @@ -0,0 +1,175 @@ +package i18n + +import ( + "testing" + + goi18n "github.com/nicksnyder/go-i18n/v2/i18n" +) + +func TestNormalizeToBCP47(t *testing.T) { + tests := []struct { + input string + expected string + }{ + // Basic cases + {"pt", "pt"}, + {"pt-BR", "pt-BR"}, + {"pt-PT", "pt-PT"}, + + // Underscore normalization + {"pt_BR", "pt-BR"}, + {"pt_PT", "pt-PT"}, + {"en_US", "en-US"}, + + // Mixed case normalization + {"pt-br", "pt-BR"}, + {"PT-BR", "pt-BR"}, + {"Pt-Br", "pt-BR"}, + {"pT-bR", "pt-BR"}, + + // Language only cases + {"EN", "en"}, + {"Pt", "pt"}, + {"ZH", "zh"}, + + // Empty string + {"", ""}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := normalizeToBCP47(tt.input) + if result != tt.expected { + t.Errorf("normalizeToBCP47(%q) = %q; want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestGetLocaleCandidates(t *testing.T) { + tests := []struct { + input string + expected []string + }{ + // Portuguese variants + {"pt-PT", []string{"pt-PT", "pt", "pt-BR"}}, // pt-BR is default for pt + {"pt-BR", []string{"pt-BR", "pt"}}, // pt-BR doesn't need default since it IS the default + {"pt", []string{"pt", "pt-BR"}}, // pt defaults to pt-BR + + // Other languages without default variants + {"en-US", []string{"en-US", "en"}}, + {"en", []string{"en"}}, + {"fr-FR", []string{"fr-FR", "fr"}}, + {"zh-CN", []string{"zh-CN", "zh"}}, + + // Empty + {"", []string{}}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := getLocaleCandidates(tt.input) + if len(result) != len(tt.expected) { + t.Errorf("getLocaleCandidates(%q) returned %d candidates; want %d", + tt.input, len(result), len(tt.expected)) + t.Errorf(" got: %v", result) + t.Errorf(" want: %v", tt.expected) + return + } + for i, candidate := range result { + if candidate != tt.expected[i] { + t.Errorf("getLocaleCandidates(%q)[%d] = %q; want %q", + tt.input, i, candidate, tt.expected[i]) + } + } + }) + } +} + +func TestPortugueseVariantLoading(t *testing.T) { + // Test that both Portuguese variants can be loaded + testCases := []struct { + locale string + desc string + }{ + {"pt", "Portuguese (defaults to Brazilian)"}, + {"pt-BR", "Brazilian Portuguese"}, + {"pt-PT", "European Portuguese"}, + {"pt_BR", "Brazilian Portuguese with underscore"}, + {"pt_PT", "European Portuguese with underscore"}, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + localizer, err := Init(tc.locale) + if err != nil { + t.Errorf("Init(%q) failed: %v", tc.locale, err) + return + } + if localizer == nil { + t.Errorf("Init(%q) returned nil localizer", tc.locale) + } + + // Try to get a message to verify it loaded correctly + msg := localizer.MustLocalize(&goi18n.LocalizeConfig{MessageID: "help_message"}) + if msg == "" { + t.Errorf("Failed to localize message for locale %q", tc.locale) + } + }) + } +} + +func TestPortugueseVariantDistinction(t *testing.T) { + // Test that pt-BR and pt-PT return different translations + localizerBR, err := Init("pt-BR") + if err != nil { + t.Fatalf("Failed to init pt-BR: %v", err) + } + + localizerPT, err := Init("pt-PT") + if err != nil { + t.Fatalf("Failed to init pt-PT: %v", err) + } + + // Check a key that should differ between variants + // "output_to_file" should be "Saída para arquivo" in pt-BR and "Saída para ficheiro" in pt-PT + msgBR := localizerBR.MustLocalize(&goi18n.LocalizeConfig{MessageID: "output_to_file"}) + msgPT := localizerPT.MustLocalize(&goi18n.LocalizeConfig{MessageID: "output_to_file"}) + + if msgBR == msgPT { + t.Errorf("pt-BR and pt-PT returned the same translation for 'output_to_file': %q", msgBR) + } + + // Verify specific expected values + if msgBR != "Saída para arquivo" { + t.Errorf("pt-BR 'output_to_file' = %q; want 'Saída para arquivo'", msgBR) + } + if msgPT != "Saída para ficheiro" { + t.Errorf("pt-PT 'output_to_file' = %q; want 'Saída para ficheiro'", msgPT) + } +} + +func TestBackwardCompatibility(t *testing.T) { + // Test that requesting "pt" still works and defaults to pt-BR + localizerPT, err := Init("pt") + if err != nil { + t.Fatalf("Failed to init 'pt': %v", err) + } + + localizerBR, err := Init("pt-BR") + if err != nil { + t.Fatalf("Failed to init 'pt-BR': %v", err) + } + + // Both should return the same Brazilian Portuguese translation + msgPT := localizerPT.MustLocalize(&goi18n.LocalizeConfig{MessageID: "output_to_file"}) + msgBR := localizerBR.MustLocalize(&goi18n.LocalizeConfig{MessageID: "output_to_file"}) + + if msgPT != msgBR { + t.Errorf("'pt' and 'pt-BR' returned different translations: %q vs %q", msgPT, msgBR) + } + + if msgPT != "Saída para arquivo" { + t.Errorf("'pt' did not default to Brazilian Portuguese. Got %q, want 'Saída para arquivo'", msgPT) + } +} diff --git a/internal/i18n/locale.go b/internal/i18n/locale.go index 026e768c..58de3d61 100644 --- a/internal/i18n/locale.go +++ b/internal/i18n/locale.go @@ -52,6 +52,18 @@ func normalizeLocale(locale string) string { // en_US -> en-US locale = strings.ReplaceAll(locale, "_", "-") + // Ensure proper BCP 47 casing: language-REGION + parts := strings.Split(locale, "-") + if len(parts) >= 2 { + // Lowercase language, uppercase region + parts[0] = strings.ToLower(parts[0]) + parts[1] = strings.ToUpper(parts[1]) + locale = strings.Join(parts[:2], "-") // Only keep language-REGION + } else if len(parts) == 1 { + // Language only, lowercase it + locale = strings.ToLower(parts[0]) + } + return locale } diff --git a/internal/i18n/locales/de.json b/internal/i18n/locales/de.json index f0d775ee..a245906f 100644 --- a/internal/i18n/locales/de.json +++ b/internal/i18n/locales/de.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "Kommentare von YouTube-Video abrufen und an Chat senden", "output_video_metadata": "Video-Metadaten ausgeben", "additional_yt_dlp_args": "Zusätzliche Argumente für yt-dlp (z.B. '--cookies-from-browser brave')", - "specify_language_code": "Sprachcode für den Chat angeben, z.B. -g=en -g=zh", + "specify_language_code": "Sprachencode für den Chat angeben, z.B. -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Website-URL zu Markdown mit Jina AI scrapen", "search_question_jina": "Suchanfrage mit Jina AI", "seed_for_lmm_generation": "Seed für LMM-Generierung", diff --git a/internal/i18n/locales/en.json b/internal/i18n/locales/en.json index 2373c34a..5503cef8 100644 --- a/internal/i18n/locales/en.json +++ b/internal/i18n/locales/en.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "Grab comments from YouTube video and send to chat", "output_video_metadata": "Output video metadata", "additional_yt_dlp_args": "Additional arguments to pass to yt-dlp (e.g. '--cookies-from-browser brave')", - "specify_language_code": "Specify the Language Code for the chat, e.g. -g=en -g=zh", + "specify_language_code": "Specify the Language Code for the chat, e.g. -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Scrape website URL to markdown using Jina AI", "search_question_jina": "Search question using Jina AI", "seed_for_lmm_generation": "Seed to be used for LMM generation", diff --git a/internal/i18n/locales/es.json b/internal/i18n/locales/es.json index 465240a6..f167175c 100644 --- a/internal/i18n/locales/es.json +++ b/internal/i18n/locales/es.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "Obtener comentarios del video de YouTube y enviar al chat", "output_video_metadata": "Salida de metadatos del video", "additional_yt_dlp_args": "Argumentos adicionales para pasar a yt-dlp (ej. '--cookies-from-browser brave')", - "specify_language_code": "Especificar el Código de Idioma para el chat, ej. -g=en -g=zh", + "specify_language_code": "Especificar el Código de Idioma para el chat, ej. -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Extraer URL del sitio web a markdown usando Jina AI", "search_question_jina": "Pregunta de búsqueda usando Jina AI", "seed_for_lmm_generation": "Semilla para ser usada en la generación LMM", diff --git a/internal/i18n/locales/fa.json b/internal/i18n/locales/fa.json index 3723ec62..5ba406d2 100644 --- a/internal/i18n/locales/fa.json +++ b/internal/i18n/locales/fa.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "دریافت نظرات از ویدیو یوتیوب و ارسال به گفتگو", "output_video_metadata": "نمایش فراداده ویدیو", "additional_yt_dlp_args": "آرگومان‌های اضافی برای ارسال به yt-dlp (مثال: '--cookies-from-browser brave')", - "specify_language_code": "تعیین کد زبان برای گفتگو، مثال: -g=en -g=zh", + "specify_language_code": "کد زبان برای گفتگو را مشخص کنید، مثلاً -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "استخراج URL وب‌سایت به markdown با استفاده از Jina AI", "search_question_jina": "سؤال جستجو با استفاده از Jina AI", "seed_for_lmm_generation": "Seed برای استفاده در تولید LMM", diff --git a/internal/i18n/locales/fr.json b/internal/i18n/locales/fr.json index dd8b634d..62345375 100644 --- a/internal/i18n/locales/fr.json +++ b/internal/i18n/locales/fr.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "Récupérer les commentaires de la vidéo YouTube et envoyer au chat", "output_video_metadata": "Afficher les métadonnées de la vidéo", "additional_yt_dlp_args": "Arguments supplémentaires à passer à yt-dlp (ex. '--cookies-from-browser brave')", - "specify_language_code": "Spécifier le code de langue pour le chat, ex. -g=en -g=zh", + "specify_language_code": "Spécifier le code de langue pour le chat, ex. -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Scraper l'URL du site web en markdown en utilisant Jina AI", "search_question_jina": "Question de recherche en utilisant Jina AI", "seed_for_lmm_generation": "Graine à utiliser pour la génération LMM", diff --git a/internal/i18n/locales/it.json b/internal/i18n/locales/it.json index 86c686f1..ba917924 100644 --- a/internal/i18n/locales/it.json +++ b/internal/i18n/locales/it.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "Ottieni commenti dal video YouTube e invia alla chat", "output_video_metadata": "Output metadati video", "additional_yt_dlp_args": "Argomenti aggiuntivi da passare a yt-dlp (es. '--cookies-from-browser brave')", - "specify_language_code": "Specifica il codice lingua per la chat, es. -g=en -g=zh", + "specify_language_code": "Specifica il codice lingua per la chat, es. -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Scraping dell'URL del sito web in markdown usando Jina AI", "search_question_jina": "Domanda di ricerca usando Jina AI", "seed_for_lmm_generation": "Seed da utilizzare per la generazione LMM", diff --git a/internal/i18n/locales/ja.json b/internal/i18n/locales/ja.json index a478165b..ad6f6834 100644 --- a/internal/i18n/locales/ja.json +++ b/internal/i18n/locales/ja.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "YouTube動画からコメントを取得してチャットに送信", "output_video_metadata": "動画メタデータを出力", "additional_yt_dlp_args": "yt-dlpに渡す追加の引数(例:'--cookies-from-browser brave')", - "specify_language_code": "チャットの言語コードを指定、例:-g=en -g=zh", + "specify_language_code": "チャットの言語コードを指定、例: -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Jina AIを使用してウェブサイトURLをマークダウンにスクレイピング", "search_question_jina": "Jina AIを使用した検索質問", "seed_for_lmm_generation": "LMM生成で使用するシード", diff --git a/internal/i18n/locales/pt.json b/internal/i18n/locales/pt-BR.json similarity index 99% rename from internal/i18n/locales/pt.json rename to internal/i18n/locales/pt-BR.json index 1298a53e..54d51b69 100644 --- a/internal/i18n/locales/pt.json +++ b/internal/i18n/locales/pt-BR.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "Obter comentários do vídeo do YouTube e enviar ao chat", "output_video_metadata": "Exibir metadados do vídeo", "additional_yt_dlp_args": "Argumentos adicionais para passar ao yt-dlp (ex. '--cookies-from-browser brave')", - "specify_language_code": "Especificar código de idioma para o chat, ex. -g=en -g=zh", + "specify_language_code": "Especificar código de idioma para o chat, ex. -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "Fazer scraping da URL do site para markdown usando Jina AI", "search_question_jina": "Pergunta de busca usando Jina AI", "seed_for_lmm_generation": "Seed para ser usado na geração LMM", diff --git a/internal/i18n/locales/pt-PT.json b/internal/i18n/locales/pt-PT.json new file mode 100644 index 00000000..b0148fdb --- /dev/null +++ b/internal/i18n/locales/pt-PT.json @@ -0,0 +1,136 @@ +{ + "html_readability_error": "usa a entrada original, porque não é possível aplicar a legibilidade HTML", + "vendor_not_configured": "o fornecedor %s não está configurado", + "vendor_no_transcription_support": "o fornecedor %s não suporta transcrição de áudio", + "transcription_model_required": "modelo de transcrição é necessário (use --transcribe-model)", + "youtube_not_configured": "YouTube não está configurado, por favor execute o procedimento de configuração", + "error_fetching_playlist_videos": "erro ao obter vídeos da playlist: %w", + "scraping_not_configured": "funcionalidade de scraping não está configurada. Por favor configure o Jina para ativar o scraping", + "could_not_determine_home_dir": "não foi possível determinar o diretório home do utilizador: %w", + "could_not_stat_env_file": "não foi possível verificar o ficheiro .env: %w", + "could_not_create_config_dir": "não foi possível criar o diretório de configuração: %w", + "could_not_create_env_file": "não foi possível criar o ficheiro .env: %w", + "could_not_copy_to_clipboard": "não foi possível copiar para a área de transferência: %v", + "file_already_exists_not_overwriting": "o ficheiro %s já existe, não será sobrescrito. Renomeie o ficheiro existente ou escolha um nome diferente", + "error_creating_file": "erro ao criar ficheiro: %v", + "error_writing_to_file": "erro ao escrever no ficheiro: %v", + "error_creating_audio_file": "erro ao criar ficheiro de áudio: %v", + "error_writing_audio_data": "erro ao escrever dados de áudio no ficheiro: %v", + "tts_model_requires_audio_output": "modelo TTS '%s' requer saída de áudio. Por favor especifique um ficheiro de saída de áudio com a flag -o (ex. -o output.wav)", + "audio_output_file_specified_but_not_tts_model": "ficheiro de saída de áudio '%s' especificado mas o modelo '%s' não é um modelo TTS. Por favor use um modelo TTS como gemini-2.5-flash-preview-tts", + "file_already_exists_choose_different": "ficheiro %s já existe. Por favor escolha um nome de ficheiro diferente ou remova o ficheiro existente", + "no_notification_system_available": "nenhum sistema de notificação disponível", + "cannot_convert_string": "não é possível converter a string %q para %v", + "unsupported_conversion": "conversão não suportada de %v para %v", + "invalid_config_path": "caminho de configuração inválido: %w", + "config_file_not_found": "ficheiro de configuração não encontrado: %s", + "error_reading_config_file": "erro ao ler ficheiro de configuração: %w", + "error_parsing_config_file": "erro ao analisar ficheiro de configuração: %w", + "error_reading_piped_message": "erro ao ler mensagem redirecionada do stdin: %w", + "image_file_already_exists": "ficheiro de imagem já existe: %s", + "invalid_image_file_extension": "extensão de ficheiro de imagem inválida '%s'. Formatos suportados: .png, .jpeg, .jpg, .webp", + "image_parameters_require_image_file": "parâmetros de imagem (--image-size, --image-quality, --image-background, --image-compression) só podem ser usados com --image-file", + "invalid_image_size": "tamanho de imagem inválido '%s'. Tamanhos suportados: 1024x1024, 1536x1024, 1024x1536, auto", + "invalid_image_quality": "qualidade de imagem inválida '%s'. Qualidades suportadas: low, medium, high, auto", + "invalid_image_background": "fundo de imagem inválido '%s'. Fundos suportados: opaque, transparent", + "image_compression_jpeg_webp_only": "compressão de imagem só pode ser usada com formatos JPEG e WebP, não %s", + "image_compression_range_error": "compressão de imagem deve estar entre 0 e 100, recebido %d", + "transparent_background_png_webp_only": "fundo transparente só pode ser usado com formatos PNG e WebP, não %s", + "available_transcription_models": "Modelos de transcrição disponíveis:", + "tts_audio_generated_successfully": "Áudio TTS gerado com sucesso e guardado em: %s\n", + "fabric_command_complete": "Comando Fabric concluído", + "fabric_command_complete_with_pattern": "Fabric: %s concluído", + "command_completed_successfully": "Comando concluído com sucesso", + "output_truncated": "Saída: %s...", + "output_full": "Saída: %s", + "choose_pattern_from_available": "Escolha um padrão dos padrões disponíveis", + "pattern_variables_help": "Valores para variáveis de padrão, ex. -v=#role:expert -v=#points:30", + "choose_context_from_available": "Escolha um contexto dos contextos disponíveis", + "choose_session_from_available": "Escolha uma sessão das sessões disponíveis", + "attachment_path_or_url_help": "Caminho do anexo ou URL (ex. para mensagens de reconhecimento de imagem do OpenAI)", + "run_setup_for_reconfigurable_parts": "Executar configuração para todas as partes reconfiguráveis do fabric", + "set_temperature": "Definir temperatura", + "set_top_p": "Definir top P", + "stream_help": "Streaming", + "set_presence_penalty": "Definir penalidade de presença", + "use_model_defaults_raw_help": "Usar as predefinições do modelo sem enviar opções de chat (como temperatura, etc.) e usar o papel de utilizador em vez do papel de sistema para padrões.", + "set_frequency_penalty": "Definir penalidade de frequência", + "list_all_patterns": "Listar todos os padrões", + "list_all_available_models": "Listar todos os modelos disponíveis", + "list_all_contexts": "Listar todos os contextos", + "list_all_sessions": "Listar todas as sessões", + "update_patterns": "Atualizar padrões", + "messages_to_send_to_chat": "Mensagens para enviar ao chat", + "copy_to_clipboard": "Copiar para área de transferência", + "choose_model": "Escolher modelo", + "specify_vendor_for_model": "Especificar fornecedor para o modelo selecionado (ex. -V \"LM Studio\" -m openai/gpt-oss-20b)", + "model_context_length_ollama": "Comprimento do contexto do modelo (afeta apenas ollama)", + "output_to_file": "Saída para ficheiro", + "output_entire_session": "Saída de toda a sessão (incluindo temporária) para o ficheiro de saída", + "number_of_latest_patterns": "Número dos padrões mais recentes a listar", + "change_default_model": "Mudar modelo predefinido", + "youtube_url_help": "Vídeo do YouTube ou \"URL\" de playlist para obter transcrição, comentários e enviar ao chat ou imprimir na consola e armazenar no ficheiro de saída", + "prefer_playlist_over_video": "Preferir playlist ao vídeo se ambos os IDs estiverem presentes na URL", + "grab_transcript_from_youtube": "Obter transcrição do vídeo do YouTube e enviar ao chat (usado por omissão).", + "grab_transcript_with_timestamps": "Obter transcrição do vídeo do YouTube com timestamps e enviar ao chat", + "grab_comments_from_youtube": "Obter comentários do vídeo do YouTube e enviar ao chat", + "output_video_metadata": "Mostrar metadados do vídeo", + "additional_yt_dlp_args": "Argumentos adicionais para passar ao yt-dlp (ex. '--cookies-from-browser brave')", + "specify_language_code": "Especificar código de idioma para o chat, ex. -g=en -g=zh -g=pt-BR -g=pt-PT", + "scrape_website_url": "Fazer scraping da URL do site para markdown usando Jina AI", + "search_question_jina": "Pergunta de pesquisa usando Jina AI", + "seed_for_lmm_generation": "Seed para ser usado na geração LMM", + "wipe_context": "Limpar contexto", + "wipe_session": "Limpar sessão", + "print_context": "Imprimir contexto", + "print_session": "Imprimir sessão", + "convert_html_readability": "Converter entrada HTML numa visualização limpa e legível", + "apply_variables_to_input": "Aplicar variáveis à entrada do utilizador", + "disable_pattern_variable_replacement": "Desabilitar substituição de variáveis de padrão", + "show_dry_run": "Mostrar o que seria enviado ao modelo sem enviar de facto", + "serve_fabric_rest_api": "Servir a API REST do Fabric", + "serve_fabric_api_ollama_endpoints": "Servir a API REST do Fabric com endpoints ollama", + "address_to_bind_rest_api": "Endereço para associar a API REST", + "api_key_secure_server_routes": "Chave API usada para proteger as rotas do servidor", + "path_to_yaml_config": "Caminho para ficheiro de configuração YAML", + "print_current_version": "Imprimir versão atual", + "list_all_registered_extensions": "Listar todas as extensões registadas", + "register_new_extension": "Registar uma nova extensão do caminho do ficheiro de configuração", + "remove_registered_extension": "Remover uma extensão registada por nome", + "choose_strategy_from_available": "Escolher uma estratégia das estratégias disponíveis", + "list_all_strategies": "Listar todas as estratégias", + "list_all_vendors": "Listar todos os fornecedores", + "output_raw_list_shell_completion": "Saída de lista simples sem cabeçalhos/formatação (para conclusão de shell)", + "enable_web_search_tool": "Habilitar ferramenta de pesquisa web para modelos suportados (Anthropic, OpenAI, Gemini)", + "set_location_web_search": "Definir localização para resultados de pesquisa web (ex. 'America/Los_Angeles')", + "save_generated_image_to_file": "Guardar imagem gerada no caminho de ficheiro especificado (ex. 'output.png')", + "image_dimensions_help": "Dimensões da imagem: 1024x1024, 1536x1024, 1024x1536, auto (por omissão: auto)", + "image_quality_help": "Qualidade da imagem: low, medium, high, auto (por omissão: auto)", + "compression_level_jpeg_webp": "Nível de compressão 0-100 para formatos JPEG/WebP (por omissão: não definido)", + "background_type_help": "Tipo de fundo: opaque, transparent (por omissão: opaque, apenas para PNG/WebP)", + "suppress_thinking_tags": "Suprimir texto contido em tags de pensamento", + "start_tag_thinking_sections": "Tag inicial para secções de pensamento", + "end_tag_thinking_sections": "Tag final para secções de pensamento", + "disable_openai_responses_api": "Desabilitar API OpenAI Responses (por omissão: false)", + "audio_video_file_transcribe": "Ficheiro de áudio ou vídeo para transcrever", + "model_for_transcription": "Modelo para usar na transcrição (separado do modelo de chat)", + "split_media_files_ffmpeg": "Dividir ficheiros de áudio/vídeo maiores que 25MB usando ffmpeg", + "tts_voice_name": "Nome da voz TTS para modelos suportados (ex. Kore, Charon, Puck)", + "list_gemini_tts_voices": "Listar todas as vozes TTS do Gemini disponíveis", + "list_transcription_models": "Listar todos os modelos de transcrição disponíveis", + "send_desktop_notification": "Enviar notificação no ambiente de trabalho quando o comando for concluído", + "custom_notification_command": "Comando personalizado para executar notificações (substitui notificações integradas)", + "set_reasoning_thinking_level": "Definir nível de raciocínio/pensamento (ex. off, low, medium, high, ou tokens numéricos para Anthropic ou Google Gemini)", + "set_debug_level": "Definir nível de debug (0=desligado, 1=básico, 2=detalhado, 3=rastreio)", + "usage_header": "Uso:", + "application_options_header": "Opções da aplicação:", + "help_options_header": "Opções de ajuda:", + "help_message": "Mostrar esta mensagem de ajuda", + "options_placeholder": "[OPÇÕES]", + "available_vendors_header": "Fornecedores disponíveis:", + "available_models_header": "Modelos disponíveis", + "no_items_found": "Nenhum %s", + "no_description_available": "Nenhuma descrição disponível", + "i18n_download_failed": "Falha ao descarregar tradução para o idioma '%s': %v", + "i18n_load_failed": "Falha ao carregar ficheiro de tradução: %v" +} diff --git a/internal/i18n/locales/zh.json b/internal/i18n/locales/zh.json index 5a33a413..95fb9938 100644 --- a/internal/i18n/locales/zh.json +++ b/internal/i18n/locales/zh.json @@ -76,7 +76,7 @@ "grab_comments_from_youtube": "从 YouTube 视频获取评论并发送到聊天", "output_video_metadata": "输出视频元数据", "additional_yt_dlp_args": "传递给 yt-dlp 的其他参数(例如 '--cookies-from-browser brave')", - "specify_language_code": "指定聊天的语言代码,例如 -g=en -g=zh", + "specify_language_code": "指定聊天的语言代码,例如 -g=en -g=zh -g=pt-BR -g=pt-PT", "scrape_website_url": "使用 Jina AI 将网站 URL 抓取为 markdown", "search_question_jina": "使用 Jina AI 搜索问题", "seed_for_lmm_generation": "用于 LMM 生成的种子",