feat(14.2.1-05): hydrate ML create/GET/save through TranslatedExact
Form schema meta lists enabled locales, Show/save expand mltext maps without D-11 fallback, and the SPA seeds and adopt-merges every locale. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,11 +12,13 @@ const (
|
||||
mlMarkdownFieldType = "mlmarkdown"
|
||||
)
|
||||
|
||||
// TranslationWriter persists non-default locale values during a cabana save.
|
||||
// The interface lives in the framework so cabana never imports a plugin; a
|
||||
// host publishes an adapter at Boot. There is no standalone translate-write
|
||||
// TranslationWriter persists non-default locale values during a cabana save
|
||||
// and reads exact stored translations for Show/save hydration. The interface
|
||||
// lives in the framework so cabana never imports a plugin; a host publishes
|
||||
// an adapter at Boot. There is no standalone translate-read or translate-write
|
||||
// HTTP endpoint: writes run only inside a permissioned, scoped host save
|
||||
// transaction after the host row has a primary key.
|
||||
// transaction after the host row has a primary key, and reads run on the
|
||||
// same adapter after projectFullRecord.
|
||||
type TranslationWriter interface {
|
||||
// DefaultLocale is the host-column locale.
|
||||
DefaultLocale(ctx context.Context, tx *gorm.DB) (string, error)
|
||||
@@ -25,6 +27,11 @@ type TranslationWriter interface {
|
||||
// WriteTranslated stores field in locale for model. locale is never the
|
||||
// default; cabana already filled the host scalar.
|
||||
WriteTranslated(ctx context.Context, tx *gorm.DB, model any, field, locale, value string) error
|
||||
// TranslatedExact returns the stored value for field in locale. ok is
|
||||
// false when the non-default key is missing. It must not apply D-11
|
||||
// fallback: empty admin values stay empty. The default locale still
|
||||
// reads the host column.
|
||||
TranslatedExact(ctx context.Context, tx *gorm.DB, model any, field, locale string) (string, bool, error)
|
||||
}
|
||||
|
||||
// mlValue is one multilingual field lifted from a save body.
|
||||
@@ -158,3 +165,68 @@ func applyMLTranslations(ctx context.Context, tx *gorm.DB, writer TranslationWri
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hydrateMLRecord replaces host scalars of declared mltext/mlmarkdown fields
|
||||
// in a Show/save RecordResult.Data map with a locale→text map for every
|
||||
// enabled code. The default locale is the projected host column; other codes
|
||||
// come from TranslatedExact; a missing non-default code is an empty string,
|
||||
// never D-11 fallback. It is a no-op when writer is nil and is not used from
|
||||
// list row projection.
|
||||
func hydrateMLRecord(ctx context.Context, tx *gorm.DB, cc *CompiledController, writer TranslationWriter, model any, data map[string]any, op string) error {
|
||||
if writer == nil || cc == nil || cc.Form == nil || data == nil {
|
||||
return nil
|
||||
}
|
||||
var names []string
|
||||
for _, field := range cc.Form.Fields {
|
||||
if !mlFieldType(field.Type) || !contextAllows(cc, field.Name, op) {
|
||||
continue
|
||||
}
|
||||
names = append(names, field.Name)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
defaultLocale, err := writer.DefaultLocale(ctx, tx)
|
||||
if err != nil || defaultLocale == "" {
|
||||
return fmt.Errorf("cabana: default locale: %w", err)
|
||||
}
|
||||
codes, err := writer.EnabledLocales(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cabana: enabled locales: %w", err)
|
||||
}
|
||||
for _, name := range names {
|
||||
host := hostScalarString(data[name])
|
||||
out := make(map[string]string, len(codes))
|
||||
for _, code := range codes {
|
||||
if code == defaultLocale {
|
||||
out[code] = host
|
||||
continue
|
||||
}
|
||||
text, ok, err := writer.TranslatedExact(ctx, tx, model, name, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
out[code] = ""
|
||||
continue
|
||||
}
|
||||
out[code] = text
|
||||
}
|
||||
data[name] = out
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostScalarString(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
case []byte:
|
||||
return string(t)
|
||||
default:
|
||||
return fmt.Sprint(t)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user