mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-10 23:08:06 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3cddb9419 | ||
|
|
cef8c567ca | ||
|
|
94e8d69dac | ||
|
|
6eee447026 | ||
|
|
17d5544df9 | ||
|
|
4715440652 | ||
|
|
d7da611a43 | ||
|
|
fa4532e9de | ||
|
|
b34112d7ed | ||
|
|
6d7585c522 | ||
|
|
2adc7b2102 | ||
|
|
a2f2d0e2d9 | ||
|
|
3e2df4b717 | ||
|
|
1bf7006224 | ||
|
|
13178456e5 | ||
|
|
079b2b5b28 | ||
|
|
e46b253cfe | ||
|
|
3a42fa7ece | ||
|
|
a302d0b46b |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -44,8 +44,6 @@ jobs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./go.mod
|
||||
- name: Install garble
|
||||
run: go install mvdan.cc/garble@latest
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
|
||||
@@ -95,6 +95,7 @@ jobs:
|
||||
run: |
|
||||
go run ./cmd/generate_changelog --process-prs ${{ steps.increment_version.outputs.new_tag }}
|
||||
go run ./cmd/generate_changelog --sync-db
|
||||
git add ./cmd/generate_changelog/changelog.db
|
||||
- name: Commit changes
|
||||
run: |
|
||||
# These files are modified by the version bump process
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# Read the documentation at https://goreleaser.com
|
||||
# For a full reference of the configuration file.
|
||||
|
||||
version: 2
|
||||
|
||||
project_name: fabric
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
# - go generate ./...
|
||||
|
||||
builds:
|
||||
- id: default
|
||||
@@ -19,22 +17,28 @@ builds:
|
||||
- linux
|
||||
main: ./cmd/fabric
|
||||
binary: fabric
|
||||
- id: windows-garbled
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.ShortCommit}}
|
||||
- -X main.date={{.Date}}
|
||||
- -X main.builtBy=goreleaser
|
||||
- -X main.tag={{.Tag}}
|
||||
|
||||
- id: windows-build
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- windows
|
||||
main: ./cmd/fabric
|
||||
binary: fabric
|
||||
tool: garble
|
||||
# From https://github.com/eyevanovich/garble-goreleaser-example/blob/main/.goreleaser.yaml
|
||||
# command is a single string.
|
||||
# garble's 'build' needs the -literals and -tiny args before it, so we
|
||||
# trick goreleaser into using -literals as command, and pass -tiny and
|
||||
# build as flags.
|
||||
command: "-literals"
|
||||
flags: [ "-tiny", "-seed=random", "build" ]
|
||||
ldflags: [ "-s", "-w" ]
|
||||
ldflags:
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.ShortCommit}}
|
||||
- -X main.date={{.Date}}
|
||||
- -X main.builtBy=goreleaser
|
||||
- -X main.tag={{.Tag}}
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
|
||||
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",
|
||||
|
||||
5416
CHANGELOG.md
5416
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.313"
|
||||
var version = "v1.4.319"
|
||||
|
||||
Binary file not shown.
@@ -180,15 +180,6 @@ func (w *Walker) WalkHistory() (map[string]*Version, error) {
|
||||
return nil, fmt.Errorf("failed to get commit log: %w", err)
|
||||
}
|
||||
|
||||
// Get the latest tag to know the boundary between released and unreleased
|
||||
latestTag, _ := w.GetLatestTag()
|
||||
var latestTagHash plumbing.Hash
|
||||
if latestTag != "" {
|
||||
if tagRef, err := w.repo.Tag(latestTag); err == nil {
|
||||
latestTagHash = tagRef.Hash()
|
||||
}
|
||||
}
|
||||
|
||||
versions := make(map[string]*Version)
|
||||
currentVersion := "Unreleased"
|
||||
versions[currentVersion] = &Version{
|
||||
@@ -197,18 +188,8 @@ func (w *Walker) WalkHistory() (map[string]*Version, error) {
|
||||
}
|
||||
|
||||
prNumbers := make(map[string][]int)
|
||||
passedLatestTag := false
|
||||
// If there's no latest tag, treat all commits as belonging to their found versions
|
||||
if latestTag == "" {
|
||||
passedLatestTag = true
|
||||
}
|
||||
|
||||
err = commitIter.ForEach(func(c *object.Commit) error {
|
||||
// Check if we've passed the latest tag boundary
|
||||
if !passedLatestTag && latestTagHash != (plumbing.Hash{}) && c.Hash == latestTagHash {
|
||||
passedLatestTag = true
|
||||
}
|
||||
|
||||
// c.Message = Summarize(c.Message)
|
||||
commit := &Commit{
|
||||
SHA: c.Hash.String(),
|
||||
@@ -222,12 +203,7 @@ func (w *Walker) WalkHistory() (map[string]*Version, error) {
|
||||
if matches := versionPattern.FindStringSubmatch(commit.Message); len(matches) > 1 {
|
||||
commit.IsVersion = true
|
||||
commit.Version = matches[1]
|
||||
|
||||
// Only change currentVersion if we're past the latest tag
|
||||
// This keeps newer commits as "Unreleased"
|
||||
if passedLatestTag {
|
||||
currentVersion = commit.Version
|
||||
}
|
||||
currentVersion = commit.Version
|
||||
|
||||
if _, exists := versions[currentVersion]; !exists {
|
||||
versions[currentVersion] = &Version{
|
||||
|
||||
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
|
||||
4
go.mod
4
go.mod
@@ -3,7 +3,7 @@ module github.com/danielmiessler/fabric
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.12.0
|
||||
github.com/anthropics/anthropic-sdk-go v1.13.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.8
|
||||
@@ -35,6 +35,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -8,6 +8,14 @@ cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcao
|
||||
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
@@ -21,6 +29,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/anthropics/anthropic-sdk-go v1.12.0 h1:xPqlGnq7rWrTiHazIvCiumA0u7mGQnwDQtvA1M82h9U=
|
||||
github.com/anthropics/anthropic-sdk-go v1.12.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||
github.com/anthropics/anthropic-sdk-go v1.13.0 h1:Bhbe8sRoDPtipttg8bQYrMCKe2b79+q6rFW1vOKEUKI=
|
||||
github.com/anthropics/anthropic-sdk-go v1.13.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
@@ -121,6 +131,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -171,6 +183,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -199,6 +213,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
||||
@@ -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 "Exportar 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 != "Exportar para arquivo" {
|
||||
t.Errorf("pt-BR 'output_to_file' = %q; want 'Exportar 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 != "Exportar para arquivo" {
|
||||
t.Errorf("'pt' did not default to Brazilian Portuguese. Got %q, want 'Exportar 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",
|
||||
@@ -133,4 +133,4 @@
|
||||
"no_description_available": "Keine Beschreibung verfügbar",
|
||||
"i18n_download_failed": "Fehler beim Herunterladen der Übersetzung für Sprache '%s': %v",
|
||||
"i18n_load_failed": "Fehler beim Laden der Übersetzungsdatei: %v"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -133,4 +133,4 @@
|
||||
"no_description_available": "توضیحی در دسترس نیست",
|
||||
"i18n_download_failed": "دانلود ترجمه برای زبان '%s' ناموفق بود: %v",
|
||||
"i18n_load_failed": "بارگذاری فایل ترجمه ناموفق بود: %v"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -133,4 +133,4 @@
|
||||
"no_description_available": "Aucune description disponible",
|
||||
"i18n_download_failed": "Échec du téléchargement de la traduction pour la langue '%s' : %v",
|
||||
"i18n_load_failed": "Échec du chargement du fichier de traduction : %v"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -133,4 +133,4 @@
|
||||
"no_description_available": "Nessuna descrizione disponibile",
|
||||
"i18n_download_failed": "Fallito il download della traduzione per la lingua '%s': %v",
|
||||
"i18n_load_failed": "Fallito il caricamento del file di traduzione: %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をマークダウンにスクレイピング",
|
||||
"search_question_jina": "Jina AIを使用した検索質問",
|
||||
"seed_for_lmm_generation": "LMM生成で使用するシード",
|
||||
@@ -133,4 +133,4 @@
|
||||
"no_description_available": "説明がありません",
|
||||
"i18n_download_failed": "言語 '%s' の翻訳のダウンロードに失敗しました: %v",
|
||||
"i18n_load_failed": "翻訳ファイルの読み込みに失敗しました: %v"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,40 +43,40 @@
|
||||
"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_pattern_from_available": "Escolha um padrão entre os padrões disponíveis",
|
||||
"pattern_variables_help": "Valores para variáveis do padrão, ex. -v=#role:expert -v=#points:30",
|
||||
"choose_context_from_available": "Escolha um contexto entre os 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",
|
||||
"attachment_path_or_url_help": "Caminho para o anexo ou URL (ex. para mensagens de reconhecimento de imagem do OpenAI)",
|
||||
"run_setup_for_reconfigurable_parts": "Executar a 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 os padrões do modelo sem enviar opções de chat (como temperatura, etc.) e usar o papel de usuário em vez do papel de sistema para padrões.",
|
||||
"use_model_defaults_raw_help": "Usar as configurações padrão do modelo sem enviar opções de chat (como temperatura, etc.) e usar o papel de usuário 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_patterns": "Listar todos os padrões/patterns",
|
||||
"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",
|
||||
"update_patterns": "Atualizar os padrões/patterns",
|
||||
"messages_to_send_to_chat": "Mensagens para enviar ao chat",
|
||||
"copy_to_clipboard": "Copiar para área de transferência",
|
||||
"copy_to_clipboard": "Copiar para a á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 arquivo",
|
||||
"output_to_file": "Exportar para arquivo",
|
||||
"output_entire_session": "Saída de toda a sessão (incluindo temporária) para o arquivo de saída",
|
||||
"number_of_latest_patterns": "Número dos padrões mais recentes a listar",
|
||||
"change_default_model": "Mudar modelo padrão",
|
||||
"youtube_url_help": "Vídeo do YouTube ou \"URL\" de playlist para obter transcrição, comentários e enviar ao chat ou imprimir no console e armazenar no arquivo de saída",
|
||||
"youtube_url_help": "Vídeo do YouTube ou URL da playlist para obter transcrição, comentários e enviar ao chat ou imprimir no console e armazenar no arquivo 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 padrã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": "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 生成的种子",
|
||||
@@ -133,4 +133,4 @@
|
||||
"no_description_available": "没有可用描述",
|
||||
"i18n_download_failed": "下载语言 '%s' 的翻译失败: %v",
|
||||
"i18n_load_failed": "加载翻译文件失败: %v"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +49,12 @@ func NewClient() (ret *Client) {
|
||||
string(anthropic.ModelClaude_3_Opus_20240229), string(anthropic.ModelClaude_3_Haiku_20240307),
|
||||
string(anthropic.ModelClaudeOpus4_20250514), string(anthropic.ModelClaudeSonnet4_20250514),
|
||||
string(anthropic.ModelClaudeOpus4_1_20250805),
|
||||
string(anthropic.ModelClaudeSonnet4_5),
|
||||
}
|
||||
|
||||
ret.modelBetas = map[string][]string{
|
||||
string(anthropic.ModelClaudeSonnet4_20250514): {"context-1m-2025-08-07"},
|
||||
string(anthropic.ModelClaudeSonnet4_5): {"context-1m-2025-08-07"},
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/plugins"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai/openai"
|
||||
openaiapi "github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
"github.com/openai/openai-go/azure"
|
||||
)
|
||||
|
||||
func NewClient() (ret *Client) {
|
||||
@@ -28,18 +29,44 @@ type Client struct {
|
||||
apiDeployments []string
|
||||
}
|
||||
|
||||
func (oi *Client) configure() (err error) {
|
||||
oi.apiDeployments = strings.Split(oi.ApiDeployments.Value, ",")
|
||||
opts := []option.RequestOption{option.WithAPIKey(oi.ApiKey.Value)}
|
||||
if oi.ApiBaseURL.Value != "" {
|
||||
opts = append(opts, option.WithBaseURL(oi.ApiBaseURL.Value))
|
||||
const defaultAPIVersion = "2024-05-01-preview"
|
||||
|
||||
func (oi *Client) configure() error {
|
||||
oi.apiDeployments = parseDeployments(oi.ApiDeployments.Value)
|
||||
|
||||
apiKey := strings.TrimSpace(oi.ApiKey.Value)
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("Azure API key is required")
|
||||
}
|
||||
if oi.ApiVersion.Value != "" {
|
||||
opts = append(opts, option.WithQuery("api-version", oi.ApiVersion.Value))
|
||||
|
||||
baseURL := strings.TrimSpace(oi.ApiBaseURL.Value)
|
||||
if baseURL == "" {
|
||||
return fmt.Errorf("Azure API base URL is required")
|
||||
}
|
||||
client := openaiapi.NewClient(opts...)
|
||||
|
||||
apiVersion := strings.TrimSpace(oi.ApiVersion.Value)
|
||||
if apiVersion == "" {
|
||||
apiVersion = defaultAPIVersion
|
||||
oi.ApiVersion.Value = apiVersion
|
||||
}
|
||||
|
||||
client := openaiapi.NewClient(
|
||||
azure.WithAPIKey(apiKey),
|
||||
azure.WithEndpoint(baseURL, apiVersion),
|
||||
)
|
||||
oi.ApiClient = &client
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDeployments(value string) []string {
|
||||
parts := strings.Split(value, ",")
|
||||
var deployments []string
|
||||
for _, part := range parts {
|
||||
if deployment := strings.TrimSpace(part); deployment != "" {
|
||||
deployments = append(deployments, deployment)
|
||||
}
|
||||
}
|
||||
return deployments
|
||||
}
|
||||
|
||||
func (oi *Client) ListModels() (ret []string, err error) {
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestClientConfigure(t *testing.T) {
|
||||
client.ApiDeployments.Value = "deployment1,deployment2"
|
||||
client.ApiKey.Value = "test-api-key"
|
||||
client.ApiBaseURL.Value = "https://example.com"
|
||||
client.ApiVersion.Value = "2021-01-01"
|
||||
client.ApiVersion.Value = "2024-05-01-preview"
|
||||
|
||||
err := client.configure()
|
||||
if err != nil {
|
||||
@@ -48,8 +48,23 @@ func TestClientConfigure(t *testing.T) {
|
||||
t.Errorf("Expected ApiClient to be initialized, got nil")
|
||||
}
|
||||
|
||||
if client.ApiVersion.Value != "2021-01-01" {
|
||||
t.Errorf("Expected API version to be '2021-01-01', got %s", client.ApiVersion.Value)
|
||||
if client.ApiVersion.Value != "2024-05-01-preview" {
|
||||
t.Errorf("Expected API version to be '2024-05-01-preview', got %s", client.ApiVersion.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientConfigureDefaultAPIVersion(t *testing.T) {
|
||||
client := NewClient()
|
||||
client.ApiDeployments.Value = "deployment1"
|
||||
client.ApiKey.Value = "test-api-key"
|
||||
client.ApiBaseURL.Value = "https://example.com"
|
||||
|
||||
if err := client.configure(); err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if client.ApiVersion.Value != defaultAPIVersion {
|
||||
t.Errorf("Expected API version to default to %s, got %s", defaultAPIVersion, client.ApiVersion.Value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ schema = 3
|
||||
[mod."dario.cat/mergo"]
|
||||
version = "v1.0.2"
|
||||
hash = "sha256-p6jdiHlLEfZES8vJnDywG4aVzIe16p0CU6iglglIweA="
|
||||
[mod."github.com/Azure/azure-sdk-for-go/sdk/azcore"]
|
||||
version = "v1.19.1"
|
||||
hash = "sha256-+cax/D2o8biQuuZkPTwTRECDPE3Ci25il9iVBcOiLC4="
|
||||
[mod."github.com/Azure/azure-sdk-for-go/sdk/internal"]
|
||||
version = "v1.11.2"
|
||||
hash = "sha256-O4Vo6D/fus3Qhs/Te644+jh2LfiG5PpiMkW0YWIbLCs="
|
||||
[mod."github.com/Microsoft/go-winio"]
|
||||
version = "v0.6.2"
|
||||
hash = "sha256-tVNWDUMILZbJvarcl/E7tpSnkn7urqgSHa2Eaka5vSU="
|
||||
@@ -26,8 +32,8 @@ schema = 3
|
||||
version = "v1.3.3"
|
||||
hash = "sha256-jv7ZshpSd7FZzKKN6hqlUgiR8C3y85zNIS/hq7g76Ho="
|
||||
[mod."github.com/anthropics/anthropic-sdk-go"]
|
||||
version = "v1.12.0"
|
||||
hash = "sha256-Oy6/7s6KHguTg2fmVGD3m0HxcaqQn1mDCUMwD5vq/eE="
|
||||
version = "v1.13.0"
|
||||
hash = "sha256-Mbi37aubaObndySkfO3tE6c8bUJaJugG1E8IM4AxW84="
|
||||
[mod."github.com/araddon/dateparse"]
|
||||
version = "v0.0.0-20210429162001-6b43995a97de"
|
||||
hash = "sha256-UuX84naeRGMsFOgIgRoBHG5sNy1CzBkWPKmd6VbLwFw="
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.313"
|
||||
"1.4.319"
|
||||
|
||||
Reference in New Issue
Block a user