Namespaces
How VitNode partitions its message tree per plugin, how a page asks for only the branches it renders, and the limits a namespace has to satisfy.
A namespace is a dotted path into the merged message tree - core.global,
@vitnode/blog.admin.article. It is the unit of two things at once: what
useTranslations reads from, and what a page is allowed to download. The merged
tree holds every installed plugin's copy, and no page should ship all of it, so a
page names the branches it renders and gets exactly those.
Example
{
"my-plugin": {
"home": { "title": "Hello World" },
"admin": { "overview": { "title": "My plugin" } }
}
}const t = useTranslations('my-plugin.home')Two namespaces exist there - my-plugin.home and my-plugin.admin.overview -
and a page that renders the public one never downloads the admin one.
The tree
Everything sits under the id of the package that owns it. Core owns three branches; a plugin owns exactly one, named after itself.
| Namespace | Owner | What is in it |
|---|---|---|
core.global | core | Design-system strings every VitNode page needs |
core.* | core | Auth, files, search, content - the public site |
admin.* | core | The AdminCP: admin.global, admin.user, admin.staff, … |
@vitnode/blog.* | @vitnode/blog | Everything that plugin ships |
{your-plugin}.* | your plugin | Everything yours ships |
core.global is special only in that it is provided above every route, so any
shared component can translate itself wherever it is mounted. Everything else is
asked for.
The per-plugin prefix rule
Every top-level key a plugin ships is the plugin's own id. A key outside it collides with core and with every other plugin, so VitNode ignores it rather than letting two packages fight over one path.
{
"my-plugin": {
"hello": "Hello World"
},
"world": "World"
}Scoped ids work exactly the same way - @vitnode/blog is one path segment even
though it contains a slash, so @vitnode/blog.admin is a legal two-segment
namespace.
Permission labels are the one exception
A staff permission's label is a flat top-level key, not a branch:
@vitnode/blog:posts:can_view. There is nothing to slice a namespace out of,
so the AdminCP asks for those keys as if they were namespaces and has to chunk
the request to stay under the limit below. If you are writing them, see Staff
permissions.
Asking for a namespace
Declare the exact branches a plugin page renders as the route's messages.
VitNode loads them with the route chunk, so public pages do not download a
plugin’s AdminCP copy just because it exists.
import { definePluginRoutes, lazy, page } from '@vitnode/core/routing'
export const routes = definePluginRoutes([
page('/reports', {
component: lazy(() => import('./pages/reports-page')),
messages: ['my-plugin.reports'],
}),
])A route inherits every namespace its layouts declare, so a shared frame can name
them once for a whole subtree. The route module then calls
useTranslations('my-plugin.reports'); if a key renders as its own name, confirm
the route declared the matching namespace.
Limits
A namespace list reaches the server through a server function, which is a public
POST endpoint once the app is built. So the rules are enforced rather than
assumed, and the same rules validate a plugin's build-time declaration - one
definition, in @vitnode/core/routing, so a route tree cannot accept something the
server refuses.
Prop
Type
On top of the numbers: a namespace must be a non-empty string, must not contain
an empty segment (core..global, a leading or trailing dot), and must not
contain __proto__, constructor or prototype in any segment. All three are
rejected outright rather than filtered away - a namespace containing one is not a
namespace with a typo in it.
Import the constants if you need to budget against them:
import { MAX_NAMESPACES } from '@vitnode/core/tanstack/i18n'Gotchas
Every string renders as its own key
The symptom of a namespace that was never asked for. Check that the route
declares it in the plugin's routes.ts.
Over sixteen namespaces means chunking, not raising the cap
At most 16 namespaces may be requested. is a thrown error, not a truncation.
If you genuinely need more - a screen rendering one flat key per permission -
split the request the way the staff screens do and merge the results, rather
than reaching for a bigger number.
Namespaces are per route, not per plugin
Name what the page renders. Declaring your plugin's whole tree on every route puts your AdminCP copy in the bundle of a public page nobody logged in is looking at.
A pendingComponent renders above its own provider
A layout route's pendingComponent renders in place of the layout, so it is
outside the RouteMessages that layout mounts. Either keep it free of
translated text or mount a provider inside it. A child route's
pendingComponent is fine - it renders into the layout's <Outlet /> , which only exists once the provider has.