v0.257.0: the app catalog can speak English (R-560, slice 5 Part A)
gates / gates (push) Successful in 25s
gates / gates (push) Successful in 25s
The READ PATH for a second language in `.felhom.yml`. An `i18n: {en: …}` sibling
block inside the same file; `Metadata.For(lang)` merges it FIELD BY FIELD over the
Hungarian, so a missing or blank English field shows the Hungarian one and a
half-translated app is a legal, shippable state.
`For("hu")` is the parsed struct with `I18n` cleared and nothing else — measured
against all 53 real catalog files, copied into `internal/stacks/testdata/catalog/`.
Lists replace whole; every other list is matched by its own key, never by position.
`For` never writes through the receiver: the metadata is the stack manager's, shared
by concurrent requests, and an in-place merge would leak one household's language
into another household's page.
Pages reach catalog copy only through `LocalizeStacks`/`LocalizeStackPtr`/`MetaFor`,
and `TestNoDirectMetaCopyReadOnPages` keeps a named, reasoned allow-list of every
direct `.Meta.<copy>` read in `internal/web` so the NEXT page to read one fails the
suite instead of quietly rendering Hungarian to an English household.
Eight red-proofs. Two of them convicted a hollow TEST rather than the code: a struct
copy shares its slices' backing arrays, so the obvious DeepEqual mutation check
passed a deliberately broken merge; and a one-entry fixture cannot tell key matching
from position matching. Both rewritten, both then seen to fail.
MinAgent: 0.131.0 (unchanged). Older controllers are unaffected — `LoadMetadata`
uses non-strict `yaml.Unmarshal`, so a pre-0.257.0 box drops the whole block.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// testdata/catalog is a SNAPSHOT of all 53 `templates/<app>/.felhom.yml` from
|
||||
// app-catalog-felhom.eu at 94bc5febaca2 (2026-09-20), copied verbatim. It is here so the identity
|
||||
// property below is measured on the REAL catalog rather than on a fixture written to pass.
|
||||
//
|
||||
// Refresh it with, from this repo's `controller/` directory:
|
||||
//
|
||||
// for d in ../../app-catalog-felhom.eu/templates/*/; do
|
||||
// a=$(basename "$d"); mkdir -p internal/stacks/testdata/catalog/$a
|
||||
// cp "$d/.felhom.yml" internal/stacks/testdata/catalog/$a/.felhom.yml
|
||||
// done
|
||||
//
|
||||
// A refresh is only ever a copy — if one of these tests then fails, the catalog has grown a shape
|
||||
// the overlay does not handle, which is the finding, not a reason to edit the fixture.
|
||||
const catalogFixtureDir = "testdata/catalog"
|
||||
|
||||
func loadCatalogFixtures(t *testing.T) map[string]Metadata {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(catalogFixtureDir)
|
||||
if err != nil {
|
||||
t.Fatalf("catalog fixtures unreadable: %v", err)
|
||||
}
|
||||
out := make(map[string]Metadata, len(entries))
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(catalogFixtureDir, e.Name(), ".felhom.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", e.Name(), err)
|
||||
}
|
||||
var m Metadata
|
||||
if err := yaml.Unmarshal(b, &m); err != nil {
|
||||
t.Fatalf("%s: %v", e.Name(), err)
|
||||
}
|
||||
out[e.Name()] = m
|
||||
}
|
||||
if len(out) < 50 {
|
||||
t.Fatalf("only %d catalog fixtures found — the snapshot is not the catalog", len(out))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestMetaForHuIsIdentity — S1. The Hungarian product must render byte-for-byte the same before and
|
||||
// after this slice (10-localisation.md §1). `For("hu")` is therefore required to be the parsed
|
||||
// struct with nothing but `I18n` cleared, over every real catalog file.
|
||||
//
|
||||
// RED-PROOF: mutating any copy field inside For's hu branch — e.g. adding
|
||||
// `out.Description = out.Description + " "` before the `lang == BaseLanguage` return — fails this
|
||||
// naming the app and the field. Run 2026-09-20: FAIL on 53 apps, then green after revert.
|
||||
func TestMetaForHuIsIdentity(t *testing.T) {
|
||||
for app, m := range loadCatalogFixtures(t) {
|
||||
want := m
|
||||
want.I18n = nil
|
||||
for _, lang := range []string{"hu", ""} {
|
||||
got := m.For(lang)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s: For(%q) changed the Hungarian metadata\n got: %+v\nwant: %+v", app, lang, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMetaForUnknownLanguageIsHungarian — a language no template carries a block for must fall all
|
||||
// the way back, not render blanks. This is the state EVERY app is in until its batch is pushed.
|
||||
func TestMetaForUnknownLanguageIsHungarian(t *testing.T) {
|
||||
for app, m := range loadCatalogFixtures(t) {
|
||||
want := m
|
||||
want.I18n = nil
|
||||
if got := m.For("en"); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s: For(\"en\") on an untranslated app is not the Hungarian view", app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCatalogFixturesCarryNoOverlayYet pins the fact the two tests above depend on: the snapshot is
|
||||
// of an UNTRANSLATED catalog, so "For(en) equals Hungarian" above is a real assertion and not a
|
||||
// tautology that would keep passing once a block exists. When the pilot lands in the catalog and the
|
||||
// snapshot is refreshed, this test fails — and the refresher must then split the fixture set into
|
||||
// translated and untranslated, rather than delete the assertion.
|
||||
func TestCatalogFixturesCarryNoOverlayYet(t *testing.T) {
|
||||
for app, m := range loadCatalogFixtures(t) {
|
||||
if len(m.I18n) != 0 {
|
||||
t.Errorf("%s: the snapshot now carries an i18n block — see this test's comment", app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// overlayFixture is one hand-written app carrying a PARTIAL English block: `description`, the
|
||||
// tagline, first_steps and ONE of two deploy fields, with the English deploy_fields list in the
|
||||
// OPPOSITE order to the Hungarian one.
|
||||
const overlayFixture = `
|
||||
display_name: "Fixture"
|
||||
description: "Magyar leírás"
|
||||
slug: fixture
|
||||
app_info:
|
||||
tagline: "Magyar tagline"
|
||||
use_cases:
|
||||
- "Magyar eset egy"
|
||||
- "Magyar eset kettő"
|
||||
first_steps:
|
||||
- "Magyar lépés egy"
|
||||
- "Magyar lépés kettő"
|
||||
prerequisites:
|
||||
- "Magyar előfeltétel"
|
||||
default_creds: "admin / admin"
|
||||
deploy_fields:
|
||||
- env_var: FIRST
|
||||
label: "Első mező"
|
||||
description: "Az első mező leírása"
|
||||
placeholder: "elso"
|
||||
type: text
|
||||
default: "alap"
|
||||
required: true
|
||||
- env_var: SECOND
|
||||
label: "Második mező"
|
||||
description: "A második mező leírása"
|
||||
type: select
|
||||
options:
|
||||
- value: "yes"
|
||||
label: "Igen"
|
||||
- value: "no"
|
||||
label: "Nem"
|
||||
i18n:
|
||||
en:
|
||||
description: "English description"
|
||||
app_info:
|
||||
tagline: "English tagline"
|
||||
first_steps:
|
||||
- "English step one"
|
||||
- "English step two"
|
||||
deploy_fields:
|
||||
- env_var: SECOND
|
||||
label: "Second field"
|
||||
options:
|
||||
- value: "no"
|
||||
label: "No"
|
||||
- env_var: FIRST
|
||||
description: "The first field explained"
|
||||
`
|
||||
|
||||
func parseFixture(t *testing.T, src string) Metadata {
|
||||
t.Helper()
|
||||
var m Metadata
|
||||
if err := yaml.Unmarshal([]byte(src), &m); err != nil {
|
||||
t.Fatalf("fixture: %v", err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// TestMetaForEnFieldByField — S2. Every English field present wins; every absent one shows the
|
||||
// Hungarian; nothing renders blank.
|
||||
func TestMetaForEnFieldByField(t *testing.T) {
|
||||
en := parseFixture(t, overlayFixture).For("en")
|
||||
|
||||
checks := []struct{ name, got, want string }{
|
||||
{"description", en.Description, "English description"},
|
||||
{"tagline", en.AppInfo.Tagline, "English tagline"},
|
||||
// Absent from the English block — the Hungarian must show through, field by field.
|
||||
{"default_creds", en.AppInfo.DefaultCreds, "admin / admin"},
|
||||
{"display_name", en.DisplayName, "Fixture"},
|
||||
}
|
||||
for _, c := range checks {
|
||||
if c.got != c.want {
|
||||
t.Errorf("%s = %q, want %q", c.name, c.got, c.want)
|
||||
}
|
||||
}
|
||||
if got := strings.Join(en.AppInfo.FirstSteps, "|"); got != "English step one|English step two" {
|
||||
t.Errorf("first_steps = %q", got)
|
||||
}
|
||||
// use_cases and prerequisites carry no English — whole Hungarian lists, not empty ones.
|
||||
if got := strings.Join(en.AppInfo.UseCases, "|"); got != "Magyar eset egy|Magyar eset kettő" {
|
||||
t.Errorf("use_cases fell back wrong: %q", got)
|
||||
}
|
||||
if got := strings.Join(en.AppInfo.Prerequisites, "|"); got != "Magyar előfeltétel" {
|
||||
t.Errorf("prerequisites fell back wrong: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeployFieldsMatchedByKey — S2's second half, and the one that would fail silently on a live
|
||||
// box: the English deploy_fields list is in the OPPOSITE order, so a position match would put
|
||||
// "Second field" on FIRST. Everything that is not copy must also survive untouched.
|
||||
func TestDeployFieldsMatchedByKey(t *testing.T) {
|
||||
en := parseFixture(t, overlayFixture).For("en")
|
||||
byVar := map[string]DeployField{}
|
||||
for _, f := range en.DeployFields {
|
||||
byVar[f.EnvVar] = f
|
||||
}
|
||||
|
||||
first, second := byVar["FIRST"], byVar["SECOND"]
|
||||
if first.Label != "Első mező" {
|
||||
t.Errorf("FIRST.label = %q — the English block does not translate it", first.Label)
|
||||
}
|
||||
if first.Description != "The first field explained" {
|
||||
t.Errorf("FIRST.description = %q", first.Description)
|
||||
}
|
||||
if first.Placeholder != "elso" {
|
||||
t.Errorf("FIRST.placeholder = %q — no English given, Hungarian expected", first.Placeholder)
|
||||
}
|
||||
if second.Label != "Second field" {
|
||||
t.Errorf("SECOND.label = %q", second.Label)
|
||||
}
|
||||
if second.Description != "A második mező leírása" {
|
||||
t.Errorf("SECOND.description = %q", second.Description)
|
||||
}
|
||||
|
||||
// Non-copy fields are not in MetadataOverlay at all, and this is the assertion that says so.
|
||||
if first.Default != "alap" || !first.Required || first.Type != "text" {
|
||||
t.Errorf("FIRST lost configuration: %+v", first)
|
||||
}
|
||||
|
||||
// Options are matched by VALUE, and only the one the block names changes.
|
||||
opts := map[string]string{}
|
||||
for _, o := range second.Options {
|
||||
opts[o.Value] = o.Label
|
||||
}
|
||||
if opts["no"] != "No" {
|
||||
t.Errorf("option no = %q, want %q", opts["no"], "No")
|
||||
}
|
||||
if opts["yes"] != "Igen" {
|
||||
t.Errorf("option yes = %q — untranslated options must stay Hungarian", opts["yes"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestEmptyEnglishFallsBack — a translator who leaves a field as `""` must not blank the page.
|
||||
// Operator ruling 3 (10-localisation.md §4): never a key, never an empty box.
|
||||
func TestEmptyEnglishFallsBack(t *testing.T) {
|
||||
m := parseFixture(t, `
|
||||
description: "Magyar leírás"
|
||||
app_info:
|
||||
tagline: "Magyar tagline"
|
||||
use_cases: ["Magyar eset"]
|
||||
i18n:
|
||||
en:
|
||||
description: ""
|
||||
app_info:
|
||||
tagline: " "
|
||||
use_cases: []
|
||||
`)
|
||||
en := m.For("en")
|
||||
if en.Description != "Magyar leírás" {
|
||||
t.Errorf("empty English description did not fall back: %q", en.Description)
|
||||
}
|
||||
if en.AppInfo.Tagline != "Magyar tagline" {
|
||||
t.Errorf("whitespace-only English tagline did not fall back: %q", en.AppInfo.Tagline)
|
||||
}
|
||||
if len(en.AppInfo.UseCases) != 1 || en.AppInfo.UseCases[0] != "Magyar eset" {
|
||||
t.Errorf("empty English list did not fall back: %v", en.AppInfo.UseCases)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForDoesNotMutateTheReceiver — the metadata For reads lives in the stack manager's cache and
|
||||
// is shared by every concurrent request. An in-place write would leak one household's language into
|
||||
// another household's page, and the page would look perfectly normal.
|
||||
// NOTE ON HOW THIS IS ASSERTED. The obvious form — copy the struct, call For, DeepEqual the copy
|
||||
// against the original — IS HOLLOW, and was written that way first: a struct copy shares the slices'
|
||||
// backing arrays, so a write through `out.DeployFields[i].Label` changes BOTH sides and the compare
|
||||
// passes. It did pass, on a deliberately broken For, in this file's red-proof run. What follows
|
||||
// therefore snapshots the concrete STRINGS before the call and compares those.
|
||||
func TestForDoesNotMutateTheReceiver(t *testing.T) {
|
||||
m := parseFixture(t, overlayFixture)
|
||||
snap := func() []string {
|
||||
out := []string{m.Description, m.AppInfo.Tagline, strings.Join(m.AppInfo.FirstSteps, "|")}
|
||||
for _, f := range m.DeployFields {
|
||||
out = append(out, f.EnvVar, f.Label, f.Description, f.Placeholder)
|
||||
for _, o := range f.Options {
|
||||
out = append(out, o.Value, o.Label)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
before := snap()
|
||||
_ = m.For("en")
|
||||
if after := snap(); !reflect.DeepEqual(before, after) {
|
||||
t.Fatalf("For(\"en\") wrote through to the receiver\nbefore: %q\n after: %q", before, after)
|
||||
}
|
||||
// And the hu view taken AFTER an English render is still Hungarian.
|
||||
if hu := m.For("hu"); hu.Description != "Magyar leírás" || hu.DeployFields[1].Options[1].Label != "Nem" {
|
||||
t.Fatalf("Hungarian view damaged by an English render: %q / %q", hu.Description, hu.DeployFields[1].Options[1].Label)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOverlayMatchesOptionalConfigIntegrationsAndDataPaths covers the three key-matched blocks the
|
||||
// main fixture leaves out. optional_config groups have no id of their own, so the overlay names the
|
||||
// Hungarian group it translates in `match_group`.
|
||||
func TestOverlayMatchesOptionalConfigIntegrationsAndDataPaths(t *testing.T) {
|
||||
// EACH LIST CARRIES TWO ENTRIES AND THE OVERLAY NAMES ONLY THE SECOND. With a single entry per
|
||||
// list — how this fixture was first written — position and key agree, and a position-matching
|
||||
// implementation passes the test. It did, in this file's red-proof run. Two entries, translated
|
||||
// out of order, is what makes the assertion mean anything.
|
||||
m := parseFixture(t, `
|
||||
optional_config:
|
||||
- group: "Egyéb"
|
||||
description: "Nem fordított csoport"
|
||||
fields:
|
||||
- env_var: OTHER_KEY
|
||||
label: "Más kulcs"
|
||||
help_text: "Más magyar súgó"
|
||||
- group: "Metaadat-szolgáltatók"
|
||||
description: "Magyar csoportleírás"
|
||||
fields:
|
||||
- env_var: KEY_ONE
|
||||
label: "Kulcs egy"
|
||||
help_text: "Magyar súgó"
|
||||
- env_var: KEY_TWO
|
||||
label: "Kulcs kettő"
|
||||
help_text: "Másik magyar súgó"
|
||||
integrations:
|
||||
- target: nextcloud
|
||||
label: "Nem fordított integráció"
|
||||
description: "Nem fordított leírás"
|
||||
- target: filebrowser
|
||||
label: "Magyar integráció"
|
||||
description: "Magyar integrációs leírás"
|
||||
data_paths:
|
||||
- path: "export"
|
||||
role: export
|
||||
label: "Nem fordított mappa"
|
||||
- path: "import"
|
||||
role: import
|
||||
label: "Magyar mappa"
|
||||
initial_credentials:
|
||||
file: /x
|
||||
note: "Magyar megjegyzés"
|
||||
i18n:
|
||||
en:
|
||||
optional_config:
|
||||
- match_group: "Metaadat-szolgáltatók"
|
||||
group: "Metadata providers"
|
||||
fields:
|
||||
- env_var: KEY_TWO
|
||||
help_text: "English help"
|
||||
integrations:
|
||||
- target: filebrowser
|
||||
label: "English integration"
|
||||
data_paths:
|
||||
- path: "import"
|
||||
label: "English folder"
|
||||
initial_credentials:
|
||||
note: "English note"
|
||||
`)
|
||||
en := m.For("en")
|
||||
|
||||
// The FIRST entry of each list is the one nobody translated — it must be untouched.
|
||||
if en.OptionalConfig[0].Group != "Egyéb" || en.OptionalConfig[0].Fields[0].HelpText != "Más magyar súgó" {
|
||||
t.Errorf("an untranslated group was changed: %+v", en.OptionalConfig[0])
|
||||
}
|
||||
if en.Integrations[0].Label != "Nem fordított integráció" {
|
||||
t.Errorf("an untranslated integration was changed: %+v", en.Integrations[0])
|
||||
}
|
||||
if en.DataPaths[0].Label != "Nem fordított mappa" {
|
||||
t.Errorf("an untranslated data_path was changed: %+v", en.DataPaths[0])
|
||||
}
|
||||
|
||||
g := en.OptionalConfig[1]
|
||||
if g.Group != "Metadata providers" {
|
||||
t.Errorf("group = %q", g.Group)
|
||||
}
|
||||
if g.Description != "Magyar csoportleírás" {
|
||||
t.Errorf("group description should fall back: %q", g.Description)
|
||||
}
|
||||
if g.Fields[1].HelpText != "English help" {
|
||||
t.Errorf("KEY_TWO help_text = %q", g.Fields[1].HelpText)
|
||||
}
|
||||
if g.Fields[0].HelpText != "Magyar súgó" || g.Fields[1].Label != "Kulcs kettő" {
|
||||
t.Errorf("unnamed optional-config fields must stay Hungarian: %+v", g.Fields)
|
||||
}
|
||||
if en.Integrations[1].Label != "English integration" || en.Integrations[1].Description != "Magyar integrációs leírás" {
|
||||
t.Errorf("integration overlay wrong: %+v", en.Integrations[1])
|
||||
}
|
||||
if en.DataPaths[1].Label != "English folder" || en.DataPaths[1].Path != "import" {
|
||||
t.Errorf("data_path overlay wrong: %+v", en.DataPaths[1])
|
||||
}
|
||||
if en.InitialCreds.Note != "English note" || en.InitialCreds.File != "/x" {
|
||||
t.Errorf("initial_credentials overlay wrong: %+v", en.InitialCreds)
|
||||
}
|
||||
|
||||
// The Hungarian view is still Hungarian afterwards — the nested slices were copied, not edited.
|
||||
hu := m.For("hu")
|
||||
if hu.OptionalConfig[1].Group != "Metaadat-szolgáltatók" || hu.OptionalConfig[1].Fields[1].HelpText != "Másik magyar súgó" {
|
||||
t.Errorf("Hungarian optional_config damaged: %+v", hu.OptionalConfig[1])
|
||||
}
|
||||
if hu.InitialCreds.Note != "Magyar megjegyzés" {
|
||||
t.Errorf("Hungarian initial_credentials note damaged: %q", hu.InitialCreds.Note)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmatchedOverlayEntriesAreInert — an English entry whose key has no Hungarian twin must
|
||||
// change nothing. (The catalog gate REFUSES such a file; this is the controller's behaviour if one
|
||||
// reaches a box anyway, e.g. from a hand-edited template.)
|
||||
func TestUnmatchedOverlayEntriesAreInert(t *testing.T) {
|
||||
m := parseFixture(t, `
|
||||
deploy_fields:
|
||||
- env_var: FIRST
|
||||
label: "Első mező"
|
||||
i18n:
|
||||
en:
|
||||
deploy_fields:
|
||||
- env_var: NOSUCHFIELD
|
||||
label: "Ghost"
|
||||
`)
|
||||
en := m.For("en")
|
||||
if len(en.DeployFields) != 1 || en.DeployFields[0].Label != "Első mező" {
|
||||
t.Fatalf("an unmatched overlay entry changed the fields: %+v", en.DeployFields)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadMetadataReadsTheI18nBlock — the loader is the seam that actually has to see `i18n:`.
|
||||
// Without this the whole overlay could be correct and inert (the "seam built but never wired"
|
||||
// class, on file four times in this project).
|
||||
func TestLoadMetadataReadsTheI18nBlock(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"), []byte(overlayFixture), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := LoadMetadata(dir)
|
||||
if len(m.I18n) != 1 {
|
||||
t.Fatalf("LoadMetadata did not parse the i18n block: %+v", m.I18n)
|
||||
}
|
||||
if got := m.For("en").Description; got != "English description" {
|
||||
t.Fatalf("loaded metadata does not localise: %q", got)
|
||||
}
|
||||
if got := m.For("hu").Description; got != "Magyar leírás" {
|
||||
t.Fatalf("loaded metadata changed Hungarian: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalizeStacksLeavesHungarianSliceAlone — LocalizeStacks returns the INPUT SLICE for hu. That
|
||||
// is the parity guarantee in code, so it is asserted rather than assumed.
|
||||
func TestLocalizeStacksLeavesHungarianSliceAlone(t *testing.T) {
|
||||
in := []Stack{{Name: "a", Meta: parseFixture(t, overlayFixture)}}
|
||||
if got := LocalizeStacks(in, "hu"); &got[0] != &in[0] {
|
||||
t.Fatalf("LocalizeStacks copied the slice for hu")
|
||||
}
|
||||
out := LocalizeStacks(in, "en")
|
||||
if out[0].Meta.Description != "English description" {
|
||||
t.Fatalf("LocalizeStacks did not localise: %q", out[0].Meta.Description)
|
||||
}
|
||||
if in[0].Meta.Description != "Magyar leírás" {
|
||||
t.Fatalf("LocalizeStacks wrote through to the input: %q", in[0].Meta.Description)
|
||||
}
|
||||
if out[0].Meta.I18n != nil {
|
||||
t.Fatalf("the untranslated half must not reach a page")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalizeStackPtrReturnsANewPointer — the manager hands out pointers to its own values; a
|
||||
// localised page must never be able to write into one.
|
||||
func TestLocalizeStackPtrReturnsANewPointer(t *testing.T) {
|
||||
st := &Stack{Name: "a", Meta: parseFixture(t, overlayFixture)}
|
||||
out := LocalizeStackPtr(st, "en")
|
||||
if out == st {
|
||||
t.Fatalf("LocalizeStackPtr returned the caller's pointer")
|
||||
}
|
||||
if st.Meta.Description != "Magyar leírás" {
|
||||
t.Fatalf("the manager's value was localised in place: %q", st.Meta.Description)
|
||||
}
|
||||
if LocalizeStackPtr(nil, "en") != nil {
|
||||
t.Fatalf("nil must stay nil")
|
||||
}
|
||||
if got := LocalizeStackPtr(st, "hu"); got.Meta.Description != "Magyar leírás" {
|
||||
t.Fatalf("hu view wrong: %q", got.Meta.Description)
|
||||
}
|
||||
if got := st.MetaFor("en"); got.Description != "English description" {
|
||||
t.Fatalf("MetaFor wrong: %q", got.Description)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user