Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/internal-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ jobs:
if [[ -n $(git status -s) ]]; then
git add .
git commit -m "docs: update docs with PTerm-CI"
git push origin HEAD:${GITHUB_REF}
if [ "${{ github.event_name }}" == "pull_request" ]; then
git push origin HEAD:${{ github.head_ref }}
else
git push origin HEAD:${GITHUB_REF}
fi
else
echo "No changes to commit"
fi
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-10-24 - Avoid strings.Split for simple line parsing when performance matters
**Learning:** Found several places where `strings.Split` is used on strings (e.g., `strings.Split(a.diff, "\n")` in `KeyExtractor`). When allocating many strings, `strings.Index` and manual slicing is faster and creates fewer allocations. The memory context mentions this specifically: "Performance convention: When parsing command output for specific markers (e.g., 'HEAD branch:'), prefer using 'strings.Index' and manual slicing over 'strings.Split' or 'strings.Scanner' to minimize allocations and processing time."
**Action:** Replace `strings.Split` with manual slicing via `strings.Index` when processing potentially large strings like diffs.
40 changes: 40 additions & 0 deletions bench2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"regexp"
"strings"
"testing"
)

var diff2 = strings.Repeat("+ t('hello')\n- ignored\n+ i18n.t(\"world\")\n", 100)

func BenchmarkRegexCurrent(b *testing.B) {
for i := 0; i < b.N; i++ {
patterns := []*regexp.Regexp{
regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`<T[^>]+key=(?:'([^']+)'|"([^"]+)")`),
regexp.MustCompile(`<T[^>]+keyName=(?:'([^']+)'|"([^"]+)")`),
}
for _, p := range patterns {
p.MatchString(diff2)
}
}
}

var globalPatterns2 = []*regexp.Regexp{
regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`<T[^>]+key=(?:'([^']+)'|"([^"]+)")`),
regexp.MustCompile(`<T[^>]+keyName=(?:'([^']+)'|"([^"]+)")`),
}

func BenchmarkRegexOptimized(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, p := range globalPatterns2 {
p.MatchString(diff2)
}
}
}
65 changes: 65 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"regexp"
"strings"
"testing"
)

var diff = strings.Repeat("+ t('hello')\n- ignored\n+ i18n.t(\"world\")\n", 1000)

var globalPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`<T[^>]+key=(?:'([^']+)'|"([^"]+)")`),
regexp.MustCompile(`<T[^>]+keyName=(?:'([^']+)'|"([^"]+)")`),
}

func BenchmarkCurrent(b *testing.B) {
for i := 0; i < b.N; i++ {
lines := strings.Split(diff, "\n")
patterns := []*regexp.Regexp{
regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
regexp.MustCompile(`<T[^>]+key=(?:'([^']+)'|"([^"]+)")`),
regexp.MustCompile(`<T[^>]+keyName=(?:'([^']+)'|"([^"]+)")`),
}

for _, line := range lines {
if !strings.HasPrefix(line, "+") {
continue
}
content := line[1:]
for _, pattern := range patterns {
pattern.FindAllStringSubmatch(content, -1)
}
}
}
}

func BenchmarkOptimized(b *testing.B) {
for i := 0; i < b.N; i++ {
remaining := diff
for len(remaining) > 0 {
var line string
idx := strings.IndexByte(remaining, '\n')
if idx >= 0 {
line = remaining[:idx]
remaining = remaining[idx+1:]
} else {
line = remaining
remaining = ""
}

if !strings.HasPrefix(line, "+") {
continue
}
content := line[1:]
for _, pattern := range globalPatterns {
pattern.FindAllStringSubmatch(content, -1)
}
}
}
}
2 changes: 1 addition & 1 deletion docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1043,4 +1043,4 @@ Run 'magi version --help' for more information on a specific command.


---
> **Documentation automatically generated with [PTerm](https://github.com/pterm/cli-template) on 06 February 2026**
> **Documentation automatically generated with [PTerm](https://github.com/pterm/cli-template) on 04 March 2026**
48 changes: 30 additions & 18 deletions internal/cli/i18n/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ type I18nKey struct {

// Agent Implementations

// Regex patterns for different i18n usage
// We use two capturing groups: one for single quotes, one for double quotes
var i18nPatterns = []*regexp.Regexp{
// t('key') or t("key")
regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
// i18n.t('key') or i18n.t("key")
regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
// $t('key') or $t("key")
regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
// <T key="key" />
regexp.MustCompile(`<T[^>]+key=(?:'([^']+)'|"([^"]+)")`),
// <T keyName="key" />
regexp.MustCompile(`<T[^>]+keyName=(?:'([^']+)'|"([^"]+)")`),
}

// KeyExtractor Agent
type KeyExtractor struct {
diff string
Expand All @@ -45,24 +60,21 @@ func (a *KeyExtractor) WaitForResults() []string {

func (a *KeyExtractor) Execute(input map[string]string) (string, error) {
var keys []I18nKey
lines := strings.Split(a.diff, "\n")

// Regex patterns for different i18n usage
// We use two capturing groups: one for single quotes, one for double quotes
patterns := []*regexp.Regexp{
// t('key') or t("key")
regexp.MustCompile(`(?:^|[^a-zA-Z0-9_])t\((?:'([^']+)'|"([^"]+)")\)`),
// i18n.t('key') or i18n.t("key")
regexp.MustCompile(`i18n\.t\((?:'([^']+)'|"([^"]+)")\)`),
// $t('key') or $t("key")
regexp.MustCompile(`\$t\((?:'([^']+)'|"([^"]+)")\)`),
// <T key="key" />
regexp.MustCompile(`<T[^>]+key=(?:'([^']+)'|"([^"]+)")`),
// <T keyName="key" />
regexp.MustCompile(`<T[^>]+keyName=(?:'([^']+)'|"([^"]+)")`),
}

for _, line := range lines {
// ⚡ Bolt: Performance optimization
// Use strings.IndexByte and manual slicing instead of strings.Split to avoid allocating a large string slice
remaining := a.diff
for len(remaining) > 0 {
var line string
idx := strings.IndexByte(remaining, '\n')
if idx >= 0 {
line = remaining[:idx]
remaining = remaining[idx+1:]
} else {
line = remaining
remaining = ""
}

// We only care about added lines
if !strings.HasPrefix(line, "+") {
continue
Expand All @@ -71,7 +83,7 @@ func (a *KeyExtractor) Execute(input map[string]string) (string, error) {
// Remove the "+" prefix
content := line[1:]

for _, pattern := range patterns {
for _, pattern := range i18nPatterns {
matches := pattern.FindAllStringSubmatch(content, -1)
for _, match := range matches {
// match[0] is full match
Expand Down