105 lines
2.9 KiB
Go
105 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
pi "github.com/joshp123/pi-golang"
|
|
)
|
|
|
|
type PiTranslator struct {
|
|
client *pi.OneShotClient
|
|
}
|
|
|
|
func NewPiTranslator(srcLang, tgtLang string, glossary []GlossaryEntry) (*PiTranslator, error) {
|
|
options := pi.DefaultOneShotOptions()
|
|
options.AppName = "openclaw-docs-i18n"
|
|
options.Mode = pi.ModeDragons
|
|
options.Dragons = pi.DragonsOptions{
|
|
Provider: "anthropic",
|
|
Model: modelVersion,
|
|
Thinking: "high",
|
|
}
|
|
options.SystemPrompt = translationPrompt(srcLang, tgtLang, glossary)
|
|
client, err := pi.StartOneShot(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &PiTranslator{client: client}, nil
|
|
}
|
|
|
|
func (t *PiTranslator) Translate(ctx context.Context, text, srcLang, tgtLang string) (string, error) {
|
|
if t.client == nil {
|
|
return "", errors.New("pi client unavailable")
|
|
}
|
|
prefix, core, suffix := splitWhitespace(text)
|
|
if core == "" {
|
|
return text, nil
|
|
}
|
|
state := NewPlaceholderState(core)
|
|
placeholders := make([]string, 0, 8)
|
|
mapping := map[string]string{}
|
|
masked := maskMarkdown(core, state.Next, &placeholders, mapping)
|
|
res, err := t.client.Run(ctx, masked)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
translated := strings.TrimSpace(res.Text)
|
|
if err := validatePlaceholders(translated, placeholders); err != nil {
|
|
return "", err
|
|
}
|
|
translated = unmaskMarkdown(translated, placeholders, mapping)
|
|
return prefix + translated + suffix, nil
|
|
}
|
|
|
|
func (t *PiTranslator) Close() {
|
|
if t.client != nil {
|
|
_ = t.client.Close()
|
|
}
|
|
}
|
|
|
|
func translationPrompt(srcLang, tgtLang string, glossary []GlossaryEntry) string {
|
|
srcLabel := srcLang
|
|
tgtLabel := tgtLang
|
|
if strings.EqualFold(srcLang, "en") {
|
|
srcLabel = "English"
|
|
}
|
|
if strings.EqualFold(tgtLang, "zh-CN") {
|
|
tgtLabel = "Simplified Chinese"
|
|
}
|
|
glossaryBlock := buildGlossaryPrompt(glossary)
|
|
return strings.TrimSpace(fmt.Sprintf(`You are a translation function, not a chat assistant.
|
|
Translate from %s to %s.
|
|
|
|
Rules:
|
|
- Output ONLY the translated text. No preamble, no questions, no commentary.
|
|
- Preserve Markdown syntax exactly (headings, lists, tables, emphasis).
|
|
- Do not translate code spans/blocks, config keys, CLI flags, or env vars.
|
|
- Do not alter URLs or anchors.
|
|
- Preserve placeholders exactly: __OC_I18N_####__.
|
|
- Use neutral technical Chinese; avoid slang or jokes.
|
|
- Keep product names in English: OpenClaw, Gateway, Pi, WhatsApp, Telegram, Discord, iMessage, Slack, Microsoft Teams, Google Chat, Signal.
|
|
|
|
%s
|
|
|
|
If the input is empty, output empty.
|
|
If the input contains only placeholders, output it unchanged.`, srcLabel, tgtLabel, glossaryBlock))
|
|
}
|
|
|
|
func buildGlossaryPrompt(glossary []GlossaryEntry) string {
|
|
if len(glossary) == 0 {
|
|
return ""
|
|
}
|
|
var lines []string
|
|
lines = append(lines, "Preferred translations (use when natural):")
|
|
for _, entry := range glossary {
|
|
if entry.Source == "" || entry.Target == "" {
|
|
continue
|
|
}
|
|
lines = append(lines, fmt.Sprintf("- %s -> %s", entry.Source, entry.Target))
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|