feat(config): internal/config (load/save atomik + validasi) — wiring bahasa/suara/animasi + doctor baca config
CI / test (push) Successful in 13m3s
CI / test (push) Successful in 13m3s
This commit is contained in:
1 parent
36bc33f904
commit
c65d2cb75d
8 files changed
+328
-15
No files matched your search
+62
-3
@@ -58,7 +58,31 @@ type tui struct {
|
|||||||
waited bool
|
waited bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTUI(t *testing.T) *tui { return startTUIEnv(t) }
|
// startTUI memakai TIA_LANG=id eksplisit — deterministik walau mesin
|
||||||
|
// pengembang/testing punya config.yaml sendiri.
|
||||||
|
func startTUI(t *testing.T) *tui { return startTUIEnv(t, "TIA_LANG=id") }
|
||||||
|
|
||||||
|
// envWith — gabung env proses + override tanpa duplikat key
|
||||||
|
// (Linux getenv memakai kemunculan pertama, jadi duplikat berbahaya).
|
||||||
|
func envWith(env []string) []string {
|
||||||
|
if len(env) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
overridden := map[string]bool{}
|
||||||
|
for _, kv := range env {
|
||||||
|
k, _, _ := strings.Cut(kv, "=")
|
||||||
|
overridden[k+"="] = true
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(os.Environ())+len(env))
|
||||||
|
for _, kv := range os.Environ() {
|
||||||
|
k, _, _ := strings.Cut(kv, "=")
|
||||||
|
if overridden[k+"="] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, kv)
|
||||||
|
}
|
||||||
|
return append(out, env...)
|
||||||
|
}
|
||||||
|
|
||||||
func startTUIEnv(t *testing.T, env ...string) *tui {
|
func startTUIEnv(t *testing.T, env ...string) *tui {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -66,8 +90,8 @@ func startTUIEnv(t *testing.T, env ...string) *tui {
|
|||||||
t.Skip("e2e pty tidak didukung di Windows")
|
t.Skip("e2e pty tidak didukung di Windows")
|
||||||
}
|
}
|
||||||
cmd := exec.Command(binPath)
|
cmd := exec.Command(binPath)
|
||||||
if len(env) > 0 {
|
if e := envWith(env); len(e) > 0 {
|
||||||
cmd.Env = append(os.Environ(), env...)
|
cmd.Env = e
|
||||||
}
|
}
|
||||||
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: rows, Cols: cols})
|
ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: rows, Cols: cols})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,6 +240,41 @@ func TestDoctorCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigFileRespected(t *testing.T) {
|
||||||
|
xdg := t.TempDir()
|
||||||
|
if err := os.MkdirAll(filepath.Join(xdg, "tia"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(xdg, "tia", "config.yaml"),
|
||||||
|
[]byte("version: 1\nui:\n language: en\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u := startTUIEnv(t, "XDG_CONFIG_HOME="+xdg)
|
||||||
|
u.waitContains("· home", 5*time.Second)
|
||||||
|
u.send("q")
|
||||||
|
if code := u.waitExit(5 * time.Second); code != 0 {
|
||||||
|
t.Fatalf("exit code %d, mau 0", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoctorUsesConfigStacksDir(t *testing.T) {
|
||||||
|
xdg := t.TempDir()
|
||||||
|
if err := os.MkdirAll(filepath.Join(xdg, "tia"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stacks := filepath.Join(xdg, "stacks-custom")
|
||||||
|
if err := os.WriteFile(filepath.Join(xdg, "tia", "config.yaml"),
|
||||||
|
[]byte("version: 1\nstacks_dir: "+stacks+"\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(binPath, "doctor")
|
||||||
|
cmd.Env = envWith([]string{"XDG_CONFIG_HOME=" + xdg})
|
||||||
|
out, _ := cmd.CombinedOutput()
|
||||||
|
if !strings.Contains(string(out), "stacks-custom") {
|
||||||
|
t.Fatalf("doctor tidak memakai stacks_dir dari config:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnglishLanguage(t *testing.T) {
|
func TestEnglishLanguage(t *testing.T) {
|
||||||
u := startTUIEnv(t, "TIA_LANG=en")
|
u := startTUIEnv(t, "TIA_LANG=en")
|
||||||
u.waitContains("· home", 5*time.Second)
|
u.waitContains("· home", 5*time.Second)
|
||||||
|
|||||||
+25
-5
@@ -11,6 +11,7 @@ import (
|
|||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
||||||
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/config"
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker"
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/sysinfo"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/sysinfo"
|
||||||
@@ -64,12 +65,24 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
|
cfg, cfgErr := config.Load()
|
||||||
|
// shortcut: TIA_LANG = override khusus dev/tes; sumber resmi bahasa
|
||||||
|
// tetap config (ui.language) — dihapus saat layar Pengaturan jadi.
|
||||||
|
if l := os.Getenv("TIA_LANG"); l != "" {
|
||||||
|
cfg.UI.Language = l
|
||||||
|
}
|
||||||
|
i18n.SetLang(cfg.UI.Language)
|
||||||
|
|
||||||
cl, err := docker.New()
|
cl, err := docker.New()
|
||||||
host, _ := os.Hostname()
|
host, _ := os.Hostname()
|
||||||
m := Model{
|
m := Model{
|
||||||
sound: audio.New(audio.Options{Enabled: true, Volume: 70, MuteOnSSH: true}),
|
sound: audio.New(audio.Options{
|
||||||
|
Enabled: cfg.UI.SoundEnabled,
|
||||||
|
Volume: cfg.UI.SoundVolume,
|
||||||
|
MuteOnSSH: cfg.UI.SoundMuteOnSSH,
|
||||||
|
}),
|
||||||
tickID: animation.NewID(),
|
tickID: animation.NewID(),
|
||||||
animating: true,
|
animating: cfg.UI.AnimationEnabled,
|
||||||
toast: components.NewToast(),
|
toast: components.NewToast(),
|
||||||
view: viewHome,
|
view: viewHome,
|
||||||
hostname: host,
|
hostname: host,
|
||||||
@@ -77,6 +90,9 @@ func New() Model {
|
|||||||
cl: cl,
|
cl: cl,
|
||||||
loading: true,
|
loading: true,
|
||||||
}
|
}
|
||||||
|
if cfgErr != nil {
|
||||||
|
m.toast.Show(i18n.T("toast.configError"), cfgErr.Error())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.loadErr = err
|
m.loadErr = err
|
||||||
m.engineErr = err
|
m.engineErr = err
|
||||||
@@ -86,10 +102,14 @@ func New() Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
if m.cl == nil {
|
var cmds []tea.Cmd
|
||||||
return animation.Next(m.tickID)
|
if m.animating {
|
||||||
|
cmds = append(cmds, animation.Next(m.tickID))
|
||||||
}
|
}
|
||||||
return tea.Batch(animation.Next(m.tickID), loadContainers(m.cl), loadInfo(m.cl))
|
if m.cl != nil {
|
||||||
|
cmds = append(cmds, loadContainers(m.cl), loadInfo(m.cl))
|
||||||
|
}
|
||||||
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadContainers(cl *docker.Client) tea.Cmd {
|
func loadContainers(cl *docker.Client) tea.Cmd {
|
||||||
|
|||||||
+16
-3
@@ -7,11 +7,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
||||||
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/config"
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/docker"
|
||||||
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
|
||||||
)
|
)
|
||||||
@@ -51,8 +51,21 @@ func Doctor(w io.Writer) int {
|
|||||||
|
|
||||||
fmt.Fprintf(w, "go : %s (%s/%s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
fmt.Fprintf(w, "go : %s (%s/%s)\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
home, _ := os.UserHomeDir()
|
// Konfigurasi & direktori stack.
|
||||||
stacks := filepath.Join(home, ".tia", "stacks")
|
cfg, cfgErr := config.Load()
|
||||||
|
cfgPath := config.Path()
|
||||||
|
switch {
|
||||||
|
case cfgErr != nil:
|
||||||
|
fmt.Fprintf(w, "config : %s %v\n", i18n.T("error.warn"), cfgErr)
|
||||||
|
default:
|
||||||
|
if _, err := os.Stat(cfgPath); err != nil {
|
||||||
|
fmt.Fprintf(w, "config : %s\n", i18n.Tf("doctor.configMissing", cfgPath))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "config : %s\n", cfgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stacks := config.ExpandPath(cfg.StacksDir)
|
||||||
if st, err := os.Stat(stacks); err == nil && st.IsDir() {
|
if st, err := os.Stat(stacks); err == nil && st.IsDir() {
|
||||||
fmt.Fprintf(w, "stacks : %s\n", stacks)
|
fmt.Fprintf(w, "stacks : %s\n", stacks)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Package config — baca/tulis config tia (PRD §7.1).
|
||||||
|
//
|
||||||
|
// Sesuai prinsip ponytail: struct ini memuat subset schema PRD §7.2 yang
|
||||||
|
// SUDAH dipakai; field lain ditambahkan saat fiturnya benar-benar masuk
|
||||||
|
// (supaya config tidak pernah berisi opsi mati).
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Version int `yaml:"version"`
|
||||||
|
StacksDir string `yaml:"stacks_dir"`
|
||||||
|
UI UI `yaml:"ui"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UI struct {
|
||||||
|
Language string `yaml:"language"` // id | en
|
||||||
|
SoundEnabled bool `yaml:"sound_enabled"`
|
||||||
|
SoundVolume int `yaml:"sound_volume"` // 0-100
|
||||||
|
SoundMuteOnSSH bool `yaml:"sound_mute_on_ssh"`
|
||||||
|
AnimationEnabled bool `yaml:"animation_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Config {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
return Config{
|
||||||
|
Version: 1,
|
||||||
|
StacksDir: filepath.Join(home, ".tia", "stacks"),
|
||||||
|
UI: UI{
|
||||||
|
Language: "id",
|
||||||
|
SoundEnabled: true,
|
||||||
|
SoundVolume: 70,
|
||||||
|
SoundMuteOnSSH: true,
|
||||||
|
AnimationEnabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path — ~/.config/tia/config.yaml (mengikuti XDG_CONFIG_HOME).
|
||||||
|
func Path() string {
|
||||||
|
dir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
dir = filepath.Join(os.Getenv("HOME"), ".config")
|
||||||
|
}
|
||||||
|
return filepath.Join(dir, "tia", "config.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load membaca config; file belum ada BUKAN error (memakai default).
|
||||||
|
func Load() (Config, error) { return LoadFrom(Path()) }
|
||||||
|
|
||||||
|
func LoadFrom(path string) (Config, error) {
|
||||||
|
c := Default()
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(b, &c); err != nil {
|
||||||
|
return Default(), fmt.Errorf("config: parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
c.normalize()
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save menulis config secara atomik (temp + rename) — tidak pernah setengah jadi.
|
||||||
|
func Save(c Config) error { return SaveTo(Path(), c) }
|
||||||
|
|
||||||
|
func SaveTo(path string, c Config) error {
|
||||||
|
c.normalize()
|
||||||
|
b, err := yaml.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("config: marshal: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmp := path + ".tmp"
|
||||||
|
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(tmp, path); err != nil {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize — validasi nilai (file config adalah batas kepercayaan:
|
||||||
|
// bisa diedit tangan; nilai rusak tidak boleh menjatuhkan aplikasi).
|
||||||
|
func (c *Config) normalize() {
|
||||||
|
switch c.UI.Language {
|
||||||
|
case "id", "en":
|
||||||
|
default:
|
||||||
|
c.UI.Language = "id"
|
||||||
|
}
|
||||||
|
if c.UI.SoundVolume < 0 {
|
||||||
|
c.UI.SoundVolume = 0
|
||||||
|
}
|
||||||
|
if c.UI.SoundVolume > 100 {
|
||||||
|
c.UI.SoundVolume = 100
|
||||||
|
}
|
||||||
|
if c.StacksDir == "" {
|
||||||
|
c.StacksDir = Default().StacksDir
|
||||||
|
}
|
||||||
|
if c.Version == 0 {
|
||||||
|
c.Version = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandPath — ubah "~/..." jadi absolut (config menyimpan path gaya user).
|
||||||
|
func ExpandPath(p string) string {
|
||||||
|
if p == "~" || strings.HasPrefix(p, "~/") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err == nil {
|
||||||
|
return filepath.Join(home, strings.TrimPrefix(strings.TrimPrefix(p, "~"), "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadMissingUsesDefaults(t *testing.T) {
|
||||||
|
c, err := LoadFrom(filepath.Join(t.TempDir(), "tidak-ada.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("file tidak ada harusnya bukan error: %v", err)
|
||||||
|
}
|
||||||
|
d := Default()
|
||||||
|
if c.UI.Language != d.UI.Language || c.UI.SoundVolume != d.UI.SoundVolume {
|
||||||
|
t.Fatalf("default salah: %+v", c.UI)
|
||||||
|
}
|
||||||
|
if c.StacksDir == "" {
|
||||||
|
t.Fatal("stacks_dir default kosong")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveLoadRoundtrip(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "sub", "config.yaml")
|
||||||
|
in := Default()
|
||||||
|
in.UI.Language = "en"
|
||||||
|
in.UI.SoundVolume = 42
|
||||||
|
in.StacksDir = "/data/stacks"
|
||||||
|
|
||||||
|
if err := SaveTo(path, in); err != nil {
|
||||||
|
t.Fatalf("SaveTo: %v", err)
|
||||||
|
}
|
||||||
|
out, err := LoadFrom(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadFrom: %v", err)
|
||||||
|
}
|
||||||
|
if out.UI.Language != "en" || out.UI.SoundVolume != 42 || out.StacksDir != "/data/stacks" {
|
||||||
|
t.Fatalf("roundtrip salah: %+v", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPartialYamlKeepsDefaults(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
os.WriteFile(path, []byte("ui:\n language: en\n"), 0o644)
|
||||||
|
|
||||||
|
c, err := LoadFrom(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadFrom: %v", err)
|
||||||
|
}
|
||||||
|
if c.UI.Language != "en" {
|
||||||
|
t.Fatalf("language: %s", c.UI.Language)
|
||||||
|
}
|
||||||
|
if c.UI.SoundVolume != 70 || !c.UI.SoundEnabled || !c.UI.AnimationEnabled {
|
||||||
|
t.Fatalf("field lain harus default: %+v", c.UI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeInvalidValues(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
os.WriteFile(path, []byte("ui:\n language: klingon\n sound_volume: 999\n"), 0o644)
|
||||||
|
|
||||||
|
c, err := LoadFrom(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadFrom: %v", err)
|
||||||
|
}
|
||||||
|
if c.UI.Language != "id" {
|
||||||
|
t.Fatalf("language rusak harus fallback id: %s", c.UI.Language)
|
||||||
|
}
|
||||||
|
if c.UI.SoundVolume != 100 {
|
||||||
|
t.Fatalf("volume harus diklamp 100: %d", c.UI.SoundVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMalformedYamlIsError(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
os.WriteFile(path, []byte("ui: [ini: rusak\n"), 0o644)
|
||||||
|
if _, err := LoadFrom(path); err == nil {
|
||||||
|
t.Fatal("yaml rusak harusnya error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandPath(t *testing.T) {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
if got := ExpandPath("~/x"); got != filepath.Join(home, "x") {
|
||||||
|
t.Fatalf("~/x = %s", got)
|
||||||
|
}
|
||||||
|
if got := ExpandPath("~"); got != home {
|
||||||
|
t.Fatalf("~ = %s", got)
|
||||||
|
}
|
||||||
|
if got := ExpandPath("/abs/path"); got != "/abs/path" {
|
||||||
|
t.Fatalf("abs berubah: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ package i18n
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -34,9 +33,6 @@ func init() {
|
|||||||
catalogs[lang] = c
|
catalogs[lang] = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l := os.Getenv("TIA_LANG"); l != "" {
|
|
||||||
active = l
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcut: bahasa hanya dari TIA_LANG + default id — akan dibaca dari
|
// shortcut: bahasa hanya dari TIA_LANG + default id — akan dibaca dari
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ toast:
|
|||||||
demoTitle: "✔ [ SUCCESS ] Conversion Saved"
|
demoTitle: "✔ [ SUCCESS ] Conversion Saved"
|
||||||
demoBody: "Stack 'web-app' saved to ~/.tia/stacks/web-app/"
|
demoBody: "Stack 'web-app' saved to ~/.tia/stacks/web-app/"
|
||||||
dockerError: "✖ [ FAILED ] Docker"
|
dockerError: "✖ [ FAILED ] Docker"
|
||||||
|
configError: "⚠ [ WARNING ] Config problem — using defaults"
|
||||||
|
|
||||||
activity:
|
activity:
|
||||||
coming: "Docker events listener coming (Phase 2)."
|
coming: "Docker events listener coming (Phase 2)."
|
||||||
@@ -84,6 +85,7 @@ doctor:
|
|||||||
dockerDaemonUnreachable: "daemon not responding: %v"
|
dockerDaemonUnreachable: "daemon not responding: %v"
|
||||||
dockerOk: "engine %s (API %s) · %d/%d containers running · %d images"
|
dockerOk: "engine %s (API %s) · %d/%d containers running · %d images"
|
||||||
stacksMissing: "%s (missing — created during onboarding)"
|
stacksMissing: "%s (missing — created during onboarding)"
|
||||||
|
configMissing: "%s (missing — using defaults)"
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
unknownCommand: "unknown command: %q"
|
unknownCommand: "unknown command: %q"
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ toast:
|
|||||||
demoTitle: "✔ [ SUKSES ] Konversi Berhasil Disimpan"
|
demoTitle: "✔ [ SUKSES ] Konversi Berhasil Disimpan"
|
||||||
demoBody: "Stack 'web-app' tersimpan di ~/.tia/stacks/web-app/"
|
demoBody: "Stack 'web-app' tersimpan di ~/.tia/stacks/web-app/"
|
||||||
dockerError: "✖ [ GAGAL ] Docker"
|
dockerError: "✖ [ GAGAL ] Docker"
|
||||||
|
configError: "⚠ [ PERINGATAN ] Config bermasalah — memakai default"
|
||||||
|
|
||||||
activity:
|
activity:
|
||||||
coming: "Listener Docker events menyusul (Phase 2)."
|
coming: "Listener Docker events menyusul (Phase 2)."
|
||||||
@@ -84,6 +85,7 @@ doctor:
|
|||||||
dockerDaemonUnreachable: "daemon tidak merespons: %v"
|
dockerDaemonUnreachable: "daemon tidak merespons: %v"
|
||||||
dockerOk: "engine %s (API %s) · %d/%d kontainer berjalan · %d image"
|
dockerOk: "engine %s (API %s) · %d/%d kontainer berjalan · %d image"
|
||||||
stacksMissing: "%s (belum ada — dibuat saat onboarding)"
|
stacksMissing: "%s (belum ada — dibuat saat onboarding)"
|
||||||
|
configMissing: "%s (belum ada — memakai default)"
|
||||||
|
|
||||||
cli:
|
cli:
|
||||||
unknownCommand: "perintah tidak dikenal: %q"
|
unknownCommand: "perintah tidak dikenal: %q"
|
||||||
|
|||||||
Reference in new issue
Block a user