mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-07 21:44:02 -05:00
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:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -18,6 +18,7 @@
|
||||
"Callirhoe",
|
||||
"Callirrhoe",
|
||||
"Cerebras",
|
||||
"colour",
|
||||
"compadd",
|
||||
"compdef",
|
||||
"compinit",
|
||||
@@ -129,6 +130,7 @@
|
||||
"opencode",
|
||||
"opencontainers",
|
||||
"openrouter",
|
||||
"organise",
|
||||
"Orus",
|
||||
"osascript",
|
||||
"otiai",
|
||||
|
||||
7
cmd/generate_changelog/incoming/1778.txt
Normal file
7
cmd/generate_changelog/incoming/1778.txt
Normal 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
140
docs/i18n-variants.md
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
175
internal/i18n/i18n_variants_test.go
Normal file
175
internal/i18n/i18n_variants_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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生成で使用するシード",
|
||||
|
||||
@@ -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",
|
||||
136
internal/i18n/locales/pt-PT.json
Normal file
136
internal/i18n/locales/pt-PT.json
Normal 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"
|
||||
}
|
||||
@@ -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 生成的种子",
|
||||
|
||||
Reference in New Issue
Block a user