]> git.earlybird.gay Git - today/commitdiff
Update database location choice, hunt down a bunch of slot-related bugs
authorearly <me@earlybird.gay>
Sat, 21 Dec 2024 00:52:27 +0000 (17:52 -0700)
committerearly <me@earlybird.gay>
Sat, 21 Dec 2024 00:52:35 +0000 (17:52 -0700)
24 files changed:
README.md
app/app.go
cmd/test/parts-nesting/index.html [new file with mode: 0644]
cmd/test/parts-nesting/main.go [new file with mode: 0644]
cmd/test/parts-nesting/my-text.go [new file with mode: 0644]
cmd/test/parts-nesting/my-text.html [new file with mode: 0644]
database/database.go
database/database_test.go
database/test/named-versions-init/named-versions-init/v0.10/migrate.sql [new file with mode: 0644]
database/test/named-versions-init/named-versions-init/v0.2/init.sql [new file with mode: 0644]
database/test/named-versions-init/named-versions-init/v0.2/migrate.sql [new file with mode: 0644]
database/test/named-versions-init/named-versions-init/v0/init.sql [new file with mode: 0644]
database/test/named-versions-init/note.txt [new file with mode: 0644]
database/test/named-versions-init/v0.10/migrate.sql [deleted file]
database/test/named-versions-init/v0.2/init.sql [deleted file]
database/test/named-versions-init/v0.2/migrate.sql [deleted file]
database/test/named-versions-init/v0/init.sql [deleted file]
include/localized.go
web/htmltree/attrs.go
web/internal/compile/compile.go
web/internal/compile/compute.go
web/internal/compile/template.go
web/page/page.go
web/part/part.go

index 05b634df1eb40bf1dd3a90ad25145e755e069bac..94b82b6e1129c0cad41655e9c973b80724b2849b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -9,3 +9,9 @@ Today is a web framework for making server-side rendered websites in Go.
 ## Installation and Requirements
 
 `go get git.earlybird.gay/today@latest`
+
+## Known Issues/Limitations
+
+| Issue                                                                                             | Documented  | Fix Planned |
+| ------------------------------------------------------------------------------------------------- | ----------- | ----------- |
+| Conditionally filling a template slot still removes the default slot if condition isn't fulfilled | Dec 20 2024 | Yes         |
index a73d41ccacc59c22c4dc32c38f75d884736c6839..68698303b36959614f5f45469308dd40c58a84d4 100644 (file)
@@ -13,6 +13,7 @@ import (
        "path"
        "path/filepath"
        "runtime"
+       "runtime/debug"
        "slices"
        "strings"
        "sync"
@@ -278,6 +279,7 @@ func (app *App) Handle(expr string, handler http.Handler) {
                        for _, middleware := range app.preHandler {
                                handler = middleware(handler)
                        }
+                       app.Logger.Debug("handling today page", "path", translatedExpr, "page", todayPage.Name(), "language", lang)
                        app.ServeMux.Handle(translatedExpr, handler)
                }
        } else {
@@ -285,6 +287,7 @@ func (app *App) Handle(expr string, handler http.Handler) {
                for _, middleware := range app.preHandler {
                        handler = middleware(handler)
                }
+               app.Logger.Debug("handling", "path", expr)
                app.ServeMux.Handle(expr, handler)
        }
 }
@@ -325,6 +328,7 @@ func (app *App) initOnce() {
                return
        }
        app.setDefaults()
+       app.Logger.Debug("handling static files")
        app.registerStatic()
 
        app.ready.Store(true)
@@ -362,6 +366,13 @@ func (app *App) ListenAndServe() error {
 
 func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        app.initOnce()
+       defer func() {
+               if x := recover(); x != nil {
+                       fmt.Println(x)
+                       fmt.Println(string(debug.Stack()))
+                       w.WriteHeader(http.StatusInternalServerError)
+               }
+       }()
        app.Logger.Debug("serving request", "host", r.Host, "path", r.URL.Path)
        app.handler.ServeHTTP(w, r)
 }
diff --git a/cmd/test/parts-nesting/index.html b/cmd/test/parts-nesting/index.html
new file mode 100644 (file)
index 0000000..c9f838e
--- /dev/null
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<head>
+    <title>Today Test Page</title>
+</head>
+<body>
+    <main>
+        <my-text :value=".header" id="my-text-header">
+            <h1 slot="value">{{ .header.Value }}</h1>
+            <p>{{ . }}</p>
+        </my-text>
+        {{ range $i, $text := .texts }}
+        <my-text :value=".">
+            {{ if lt $i 3 }}<h2 slot="value">{{ .Value }}</h2>{{ end }}
+            <p>This is a my-text element with value {{ .Value }}!</p>
+        </my-text>
+        {{ end }}
+    </main>
+</body>
\ No newline at end of file
diff --git a/cmd/test/parts-nesting/main.go b/cmd/test/parts-nesting/main.go
new file mode 100644 (file)
index 0000000..2ca3c12
--- /dev/null
@@ -0,0 +1,49 @@
+package main
+
+import (
+       "context"
+       "fmt"
+       "log/slog"
+       "os"
+       "text/template"
+
+       tapp "git.earlybird.gay/today/app"
+       "git.earlybird.gay/today/web/page"
+       "git.earlybird.gay/today/web/render"
+)
+
+var Index = page.New("Index", "index.html",
+       page.OnLoad(func(ctx context.Context, data render.Data) error {
+               data.Set("header", MyTextType{Value: "Hello, World!"})
+               texts := make([]MyTextType, 0)
+               for i := range 5 {
+                       texts = append(texts, MyTextType{Value: fmt.Sprintf("%d", i)})
+               }
+               data.Set("texts", texts)
+               return nil
+       }),
+       page.Includes(MyText),
+       page.Funcs(template.FuncMap{
+               "log": fmt.Println,
+       }),
+       page.Pretty("  "),
+)
+
+func init() {
+       fmt.Println(Index.Raw(Index.DefaultLanguage()))
+       if Index.Error() != nil {
+               panic(Index.Error())
+       }
+}
+
+func main() {
+       app := tapp.New()
+       app.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+               Level: slog.LevelDebug,
+       }))
+       app.ShutdownOnSignal(os.Interrupt)
+
+       app.Handle("/{$}", Index)
+
+       app.ListenAndServe()
+}
diff --git a/cmd/test/parts-nesting/my-text.go b/cmd/test/parts-nesting/my-text.go
new file mode 100644 (file)
index 0000000..c30604f
--- /dev/null
@@ -0,0 +1,26 @@
+package main
+
+import (
+       "context"
+       "fmt"
+
+       "git.earlybird.gay/today/web/part"
+       "git.earlybird.gay/today/web/render"
+)
+
+type MyTextType struct {
+       Value string
+}
+
+var MyText = part.New("my-text", "my-text.html",
+       part.OnLoad(func(ctx context.Context, data render.Data) error {
+               fmt.Println(data.Get("value"))
+               return nil
+       }),
+)
+
+func init() {
+       if MyText.Error() != nil {
+               panic(MyText.Error())
+       }
+}
diff --git a/cmd/test/parts-nesting/my-text.html b/cmd/test/parts-nesting/my-text.html
new file mode 100644 (file)
index 0000000..86f8d16
--- /dev/null
@@ -0,0 +1,3 @@
+<template>
+    <slot name="value"><p class="default-text">{{ .value.Value }}</p></slot>
+</template>
\ No newline at end of file
index f197acc6efc27dd30d89a43b54eb996b66e8c95d..25b9352f978d0c805d0cd428f0e220049e520177 100644 (file)
@@ -3,7 +3,6 @@ package database
 import (
        "database/sql"
        "errors"
-       "fmt"
        "io"
        "io/fs"
        "os"
@@ -62,11 +61,23 @@ func Subdirectory(dir string) Config {
        }
 }
 
-func New(name string, confs ...Config) *Database {
+// New creates a new Database.
+// If rootDir is given and isn't absolute, it's a relative reference to
+// the current file. If it's given and absolute, it's used as the root directory
+// for the database and version. If it's not given, the root directory
+// is the directory containing the current file.
+func New(name, rootDir string, confs ...Config) *Database {
        db := &Database{
                name: name,
        }
-       db.rootDir, _ = include.Dir("git.earlybird.gay/today/database")
+       if rootDir == "" {
+               db.rootDir, _ = include.Dir("git.earlybird.gay/today/database")
+       } else if !filepath.IsAbs(rootDir) {
+               db.rootDir, _ = include.Dir("git.earlybird.gay/today/database")
+               db.rootDir = filepath.Join(db.rootDir, rootDir)
+       } else {
+               db.rootDir = rootDir
+       }
 
        for _, conf := range confs {
                db.err = errors.Join(db.err, conf(db))
@@ -126,7 +137,6 @@ func New(name string, confs ...Config) *Database {
                if !entry.IsDir() {
                        continue
                }
-               fmt.Println("entry", entry.Name())
                if submatches := versionRegexp.FindAllStringSubmatch(entry.Name(), -1); len(submatches) == 1 {
                        dbFolder := submatches[0]
                        dbf, dbfName := dbFolder[0], dbFolder[1]
index d4c3145e7cbe04728d691443e2eccc0afe16734f..cde8388b70a7286b0dcd06b806469ca9bb826144 100644 (file)
@@ -6,8 +6,7 @@ import (
 )
 
 func TestRawInit(t *testing.T) {
-       db := New("raw-init",
-               Subdirectory("test/raw-init"),
+       db := New("raw-init", "test/raw-init",
                Init(`
 CREATE TABLE messages (
        user TEXT,
@@ -23,8 +22,7 @@ INSERT INTO messages (user, msg, likes) VALUES ('test-user', 'hello, world', 5);
 }
 
 func TestFileInit(t *testing.T) {
-       db := New("file-init",
-               Subdirectory("test/file-init"),
+       db := New("file-init", "test/file-init",
                Init(`init.sql`),
        )
        if db.Error() != nil {
@@ -34,9 +32,7 @@ func TestFileInit(t *testing.T) {
 }
 
 func TestVersionsInit(t *testing.T) {
-       db := New("versions-init",
-               Subdirectory("test/versions-init"),
-       )
+       db := New("versions-init", "test/versions-init")
        if db.Error() != nil {
                t.Fatal(db.Error())
        }
@@ -44,9 +40,7 @@ func TestVersionsInit(t *testing.T) {
 }
 
 func TestNamedVersionsInit(t *testing.T) {
-       db := New("named-versions-init",
-               Subdirectory("test"),
-       )
+       db := New("named-versions-init", "test/named-versions-init")
        if db.Error() != nil {
                t.Fatal(db.Error())
        }
diff --git a/database/test/named-versions-init/named-versions-init/v0.10/migrate.sql b/database/test/named-versions-init/named-versions-init/v0.10/migrate.sql
new file mode 100644 (file)
index 0000000..95c8d19
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE messages ADD likes INTEGER;
+UPDATE messages SET likes = 5;
\ No newline at end of file
diff --git a/database/test/named-versions-init/named-versions-init/v0.2/init.sql b/database/test/named-versions-init/named-versions-init/v0.2/init.sql
new file mode 100644 (file)
index 0000000..646d20c
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE messages (
+       user TEXT,
+       msg TEXT                
+);
\ No newline at end of file
diff --git a/database/test/named-versions-init/named-versions-init/v0.2/migrate.sql b/database/test/named-versions-init/named-versions-init/v0.2/migrate.sql
new file mode 100644 (file)
index 0000000..48adf09
--- /dev/null
@@ -0,0 +1 @@
+INSERT INTO messages (user, msg) SELECT 'test-user', msg FROM old.messages;
\ No newline at end of file
diff --git a/database/test/named-versions-init/named-versions-init/v0/init.sql b/database/test/named-versions-init/named-versions-init/v0/init.sql
new file mode 100644 (file)
index 0000000..1bcf722
--- /dev/null
@@ -0,0 +1,4 @@
+CREATE TABLE messages (
+       msg TEXT
+);
+INSERT INTO messages (msg) VALUES ('hello, world');
\ No newline at end of file
diff --git a/database/test/named-versions-init/note.txt b/database/test/named-versions-init/note.txt
new file mode 100644 (file)
index 0000000..4ae8a9f
--- /dev/null
@@ -0,0 +1,3 @@
+named-versions-init is nested so that the test can
+reference the first named-versions-init folder, AND
+the database can be named named-versions-init.
diff --git a/database/test/named-versions-init/v0.10/migrate.sql b/database/test/named-versions-init/v0.10/migrate.sql
deleted file mode 100644 (file)
index 95c8d19..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-ALTER TABLE messages ADD likes INTEGER;
-UPDATE messages SET likes = 5;
\ No newline at end of file
diff --git a/database/test/named-versions-init/v0.2/init.sql b/database/test/named-versions-init/v0.2/init.sql
deleted file mode 100644 (file)
index 646d20c..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-CREATE TABLE messages (
-       user TEXT,
-       msg TEXT                
-);
\ No newline at end of file
diff --git a/database/test/named-versions-init/v0.2/migrate.sql b/database/test/named-versions-init/v0.2/migrate.sql
deleted file mode 100644 (file)
index 48adf09..0000000
+++ /dev/null
@@ -1 +0,0 @@
-INSERT INTO messages (user, msg) SELECT 'test-user', msg FROM old.messages;
\ No newline at end of file
diff --git a/database/test/named-versions-init/v0/init.sql b/database/test/named-versions-init/v0/init.sql
deleted file mode 100644 (file)
index 1bcf722..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-CREATE TABLE messages (
-       msg TEXT
-);
-INSERT INTO messages (msg) VALUES ('hello, world');
\ No newline at end of file
index 87abc205af1bc78ba0a45cbf2d85422995a69dba..a29a8f2e5fc48208f46b2177a69c968f0877ec2c 100644 (file)
@@ -72,7 +72,7 @@ findLanguages:
                                }
                                return map[language.Tag]Opener{
                                        DefaultLanguage: file,
-                               }, nil, err
+                               }, nil, nil
                        }
                }
                dir = dir[:strings.LastIndexByte(dir, filepath.Separator)]
index 3f337bd6e530fd314031285986dd18ac58ffd3b8..6e25fe61798f4d27f85a0b7cef2987117247833f 100644 (file)
@@ -1,7 +1,9 @@
 // Copyright (C) 2024 early (LGPL)
 package htmltree
 
-import "git.earlybird.gay/today/web/internal/html"
+import (
+       "git.earlybird.gay/today/web/internal/html"
+)
 
 func GetAttr(n *html.Node, name string) string {
        for _, attr := range n.Attr {
index 8ae35b4355145bd47d925e7669a6eb023dcbd64d..0081d2bd2fd4cb731b617ed6c2eca85a25fa8801 100644 (file)
@@ -111,6 +111,11 @@ func Compile(root TemplateSource, transform ...func(root *html.Node)) ([]Result,
                                                for fname, f := range templateSource.TemplateFuncs() {
                                                        templateFuncs[snakeCase(n.Data)+"_"+fname] = f
                                                }
+                                               // Add $today_parent before template sources
+                                               n.Parent.InsertBefore(&html.Node{
+                                                       Type: html.TextNode,
+                                                       Data: "{{ $today_parent := . }}",
+                                               }, n)
                                                // Parse HTML fragment and replace the content of the node with it
                                                computeSubSource, err := insertTemplateSource(lang, templateSource, n)
                                                if err != nil {
index 883b888968297ccbb7ff0094716fcff4bdb7bf69..ddc3256a8acce46aa3c37c47d7dfcd7b5315dd08 100644 (file)
@@ -15,16 +15,10 @@ type computeNode struct {
        htmlId           string
        compute          render.OnLoadFunc
        asDataFromParent map[string]string
-       raiseFromParent  []string
        children         []*computeNode
 }
 
 func (root *computeNode) Compute(r *http.Request) (render.Data, func(any) error, error) {
-       impose := func(cData, pData render.Data, raise []string) {
-               for _, key := range raise {
-                       cData.Set(key, pData.Get(key))
-               }
-       }
        var dot any
        setDot := func(val any) error {
                dot = val
@@ -65,10 +59,6 @@ func (root *computeNode) Compute(r *http.Request) (render.Data, func(any) error,
                                        }
                                        childData.Set(dataKey, val)
                                }
-                               // Raise parent variables to the "parent" var
-                               childParentData := make(render.Data)
-                               impose(childParentData, data, child.raiseFromParent)
-                               childData.Set("parent", childParentData)
                                f(child, childData)
                                return childData, child.compute(ctx, childData)
                        })
index 733cafe40dee826276dd5313437134f6f5cdecde..6b6f4c2d24d12313ac3e75227d425cd946e0e67d 100644 (file)
@@ -19,7 +19,7 @@ const (
 )
 
 var funcRegexp = regexp.MustCompile(`^\(?([a-zA-Z]\w*)\)?$`)
-var fieldRegexp = regexp.MustCompile(`^(?:\.[a-zA-Z]\w*)+$`)
+var fieldRegexp = regexp.MustCompile(`^(?:\.[a-zA-Z]\w*)+$|^\.$`)
 var scopesDownRegexp = regexp.MustCompile(`[\{\s]end[\}\s]`)
 var setsDotRegexp = regexp.MustCompile(`^\{\{-?\s*(?:with|range).*?\}\}$`)
 var templateRegexp = regexp.MustCompile(`\{\{.*\}\}`)
@@ -224,11 +224,8 @@ func replaceTemplateFuncs(source TemplateSource, n *html.Node) {
                                if funcName == "" {
                                        goto done
                                }
-                               // fmt.Println("func", token)
-                               // fmt.Println(source.TemplateFuncs())
                                // Skip anything that the target source doesn't have as a templatefunc
                                if _, ok := source.TemplateFuncs()[funcName]; ok {
-                                       // fmt.Println("found", token)
                                        namespaced := snakeCase(source.Name()) + "_" + funcName
                                        token = strings.Replace(token, funcName, namespaced, -1)
                                }
@@ -246,14 +243,13 @@ func replaceTemplateFuncs(source TemplateSource, n *html.Node) {
 }
 
 // Sets template fields (.field) in any template strings in this node to
-// .parent.field.
+// $today_parent.field.
 // Returns a list of raised fields.
-func replaceTemplateFields(n *html.Node) []string {
-       var done, inTemplate, inAttrs bool
+func replaceTemplateFields(n *html.Node) {
+       var inAttrs bool
        var datas []string
        var isDataAttr []bool
        var sets []func(string)
-       var raised []string
 
        switch n.Type {
        case html.TextNode:
@@ -272,6 +268,7 @@ func replaceTemplateFields(n *html.Node) []string {
                }
        }
        for i, data := range datas {
+               var done, inTemplate bool
                var output []string
                for {
                        endToken := strings.Index(data, " ")
@@ -292,10 +289,14 @@ func replaceTemplateFields(n *html.Node) []string {
                        // with a namespaced version
                        if inTemplate && isField(token) ||
                                inAttrs && isDataAttr[i] {
-                               raised = append(raised, token[1:])
-                               token = ".parent" + token
+                               // special case; . is parentable but shouldn't be appended
+                               fmt.Println("replacing token", token)
+                               if token == "." {
+                                       token = "$today_parent"
+                               } else {
+                                       token = "$today_parent" + token
+                               }
                        }
-
                        output = append(output, token)
                        if done {
                                break
@@ -304,8 +305,6 @@ func replaceTemplateFields(n *html.Node) []string {
                }
                sets[i](strings.Join(output, " "))
        }
-
-       return raised
 }
 
 // Removes data attributes :name.
@@ -330,18 +329,28 @@ func removeDataAttrs(n *html.Node) map[string]string {
 func insertTemplateSource(lang language.Tag, subSource TemplateSource, context *html.Node) (*computeNode, error) {
        computedName := snakeCase(subSource.Name())
        // ===== CONTEXT PREPROCESSING =====
-       // Raise any fields pre-existing in the context to .parent.field.
-       raiseFields := make([]string, 0)
+       // Raise any fields pre-existing in the context to $today_parent.field.
        raiseDepth := 0
+       raiseStack := []string{}
        htmltree.Walk(context.FirstChild, func(n *html.Node) (bool, error) {
                if raiseDepth == 0 {
-                       raiseFields = append(raiseFields, replaceTemplateFields(n)...)
+                       fmt.Println("replace", n.Data)
+                       replaceTemplateFields(n)
+                       fmt.Println("result", n.Data)
                }
                if n.Type == html.TextNode {
+                       // Scope up/down only when setting dot
+                       // Make a better way to do this
                        if scopesUp(n.Data) {
-                               raiseDepth++
+                               if setsDot(n.Data) {
+                                       raiseDepth++
+                               }
+                               raiseStack = append(raiseStack, n.Data)
                        } else if scopesDown(n.Data) {
-                               raiseDepth--
+                               if setsDot(raiseStack[len(raiseStack)-1]) {
+                                       raiseDepth--
+                               }
+                               raiseStack = raiseStack[:len(raiseStack)-1]
                        }
                }
                return false, nil
@@ -508,7 +517,6 @@ func insertTemplateSource(lang language.Tag, subSource TemplateSource, context *
                htmlId:           htmlId,
                compute:          subSource.OnLoad(),
                asDataFromParent: asData,
-               raiseFromParent:  raiseFields,
        }
 
        // Insert scope up/down template pipeline
index 608c714420f2482994d82c14f308a2da1c1d83bb..3aadeb05a18fdb24ddab31a318c18b83c7c7a01e 100644 (file)
@@ -31,7 +31,7 @@ type Page struct {
 
        templateFuncs template.FuncMap
 
-       raw              string
+       raws             map[language.Tag]string
        chooseRenderable func(r *http.Request) *renderable
 
        err error
@@ -90,9 +90,14 @@ func New(name string, source string, optional ...Config) *Page {
                p.err = err
                return p
        }
-       p.languages = localeMeta.Languages
-       p.translations = localeMeta.Translations
-       p.defaultLanguage = localeMeta.DefaultLanguage
+       if localeMeta != nil {
+               p.languages = localeMeta.Languages
+               p.translations = localeMeta.Translations
+               p.defaultLanguage = localeMeta.DefaultLanguage
+       } else {
+               p.languages = []language.Tag{language.English}
+               p.defaultLanguage = language.English
+       }
        p.sources = sources
        p.onLoad = func(ctx context.Context, d render.Data) error {
                return nil
@@ -114,8 +119,10 @@ func New(name string, source string, optional ...Config) *Page {
                return p
        }
 
+       p.raws = make(map[language.Tag]string)
        renderables := make(map[language.Tag]*renderable)
        for _, result := range results {
+               p.raws[result.Language] = result.TemplateRaw
                // templatize
                tmpl, err := template.New(p.name).
                        Funcs(result.TemplateFuncs).
@@ -129,20 +136,15 @@ func New(name string, source string, optional ...Config) *Page {
                        templateLoad: result.TemplateDataLoader,
                }
        }
-       // result := results[0]
-       // p.raw = result.TemplateRaw
-       // p.templateLoad = result.TemplateDataLoader
-
-       // selector := language.NewMatcher(localeMeta.Languages)
        p.chooseRenderable = func(r *http.Request) *renderable {
                lang, ok := r.Context().Value(localization.Locale).(language.Tag)
                if !ok {
-                       return renderables[localeMeta.DefaultLanguage]
+                       return renderables[p.defaultLanguage]
                }
                if chosen, ok := renderables[lang]; ok {
                        return chosen
                } else {
-                       return renderables[localeMeta.DefaultLanguage]
+                       return renderables[p.defaultLanguage]
                }
        }
        return p
@@ -183,6 +185,7 @@ func Static(source string) *Page {
 
        renderables := make(map[language.Tag]*renderable)
        for _, result := range results {
+               p.raws[result.Language] = result.TemplateRaw
                // templatize
                tmpl, err := template.New(p.name).
                        Funcs(result.TemplateFuncs).
@@ -286,8 +289,8 @@ func (p *Page) Includes() []compile.Source {
        return p.includes
 }
 
-func (p *Page) Raw() string {
-       return p.raw
+func (p *Page) Raw(lang language.Tag) string {
+       return p.raws[lang]
 }
 
 func (p *Page) Error() error {
index 06f41057ce3a2ec52f7986f59be5618f956b58cb..0e00bb1daa9787fd8ac8379f670ca2f3d65ebbab 100644 (file)
@@ -85,9 +85,14 @@ func New(name string, source string, optional ...Config) *Part {
                p.err = err
                return p
        }
-       p.languages = localeMeta.Languages
-       p.translations = localeMeta.Translations
-       p.defaultLanguage = localeMeta.DefaultLanguage
+       if localeMeta != nil {
+               p.languages = localeMeta.Languages
+               p.translations = localeMeta.Translations
+               p.defaultLanguage = localeMeta.DefaultLanguage
+       } else {
+               p.languages = []language.Tag{language.English}
+               p.defaultLanguage = language.English
+       }
        p.sources = sources
        if p.err != nil {
                return p