feat(i18n): add i18n support for language variants (pt-BR/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
• Update help text to show variant examples
• Add comprehensive test suite for variants
• Create documentation for i18n variant architecture
This commit is contained in:
Kayvan Sylvan
2025-09-21 15:54:36 -07:00
parent 6d7585c522
commit b34112d7ed
16 changed files with 570 additions and 18 deletions

View File

@@ -18,6 +18,7 @@
"Callirhoe",
"Callirrhoe",
"Cerebras",
"colour",
"compadd",
"compdef",
"compinit",
@@ -129,6 +130,7 @@
"opencode",
"opencontainers",
"openrouter",
"organise",
"Orus",
"osascript",
"otiai",

View File

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

140
docs/i18n-variants.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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生成で使用するシード",

View File

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

View File

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

View File

@@ -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 生成的种子",