11<script setup lang="ts">
22import { computed } from " vue" ;
33import { Sparkles , User } from " lucide-vue-next" ;
4+ import en from " ./locales/en.json" ;
5+ import de_DE from " ./locales/de_DE.json" ;
46import " ./style.css" ;
57
68// The panel injects the authenticated 5stack user here. It is null for guests
@@ -11,8 +13,49 @@ import "./style.css";
1113const props = defineProps <{
1214 user? : { steam_id: string ; name: string ; role: string } | null ;
1315 query? : Record <string , unknown >;
16+ // Capabilities the host OWNS and therefore hands down. You cannot import
17+ // these: a plugin is a separate module graph, and both `useToast` and
18+ // vue-i18n keep their state in module scope — an imported copy would be a
19+ // second, empty instance whose toasts nothing renders and whose translator
20+ // knows no messages. It fails silently, which is the worst way to fail.
21+ // See /plugins/translations.
22+ notify? : (message : string , kind : " error" | " success" ) => void ;
23+ t? : (key : string , named ? : Record <string , unknown >) => string ;
24+ locale? : string ;
1425}>();
1526
27+ // ---- translations -----------------------------------------------------------
28+ // Your own catalogue first, the panel second, a literal English fallback last.
29+ // Ship whatever languages you have; anything missing falls through to your
30+ // English, so a plugin with only en.json still works everywhere — it is just
31+ // untranslated, which beats a screen full of dotted keys.
32+ const CATALOGUES: Record <string , Record <string , unknown >> = { en , de_DE };
33+
34+ function lookup(catalogue : Record <string , unknown >, key : string ) {
35+ return key .split (" ." ).reduce <unknown >((o , k ) => (o as never )?.[k ], catalogue );
36+ }
37+
38+ function tr(key : string , fallback : string , named ? : Record <string , unknown >) {
39+ // Read locale so computeds using tr() re-run on a language switch — `t` is a
40+ // stable reference and would not invalidate them by itself.
41+ const lang = props .locale ?? " en" ;
42+ const mine = lookup (CATALOGUES [lang ] ?? {}, key ) ?? lookup (CATALOGUES .en , key );
43+ if (typeof mine === " string" ) {
44+ return mine .replace (/ \{ (\w + )\} / g , (_ , k ) => String (named ?.[k ] ?? ` {${k }} ` ));
45+ }
46+ // vue-i18n returns the KEY when a message is missing, so treat that as a miss
47+ // — otherwise "hello.title" ships to production looking like a bug.
48+ const host = props .t ?.(key , named );
49+ return ! host || host === key ? fallback : host ;
50+ }
51+
52+ // The panel's toast when embedded; a console line when running standalone.
53+ function sayHi() {
54+ const message = tr (" hello.toast_hi" , " Hello from the plugin." );
55+ if (props .notify ) props .notify (message , " success" );
56+ else console .log (message );
57+ }
58+
1659// Because the manifest sets "profileTabLabel", this same component is ALSO
1760// mounted as a tab on /players/:steamid. Two query keys tell us we are there.
1861
@@ -65,14 +108,28 @@ const embed = computed(() => props.query?.embed === "1");
65108 >
66109 <Sparkles class="h-10 w-10 text-primary" />
67110 <h1 class =" text-3xl font-bold tracking-tight text-foreground" >
68- Hello from a 5stack plugin
111+ {{ tr("hello.title", " Hello from a 5stack plugin") }}
69112 </h1 >
70113 <p class =" text-muted-foreground" >
71114 <template v-if =" user " >
72- Signed in as {{ user.name }} ({{ user.role }})
115+ {{
116+ tr("hello.signed_in", `Signed in as ${user.name} (${user.role})`, {
117+ name: user.name,
118+ role: user.role,
119+ })
120+ }}
121+ </template >
122+ <template v-else >
123+ {{ tr("hello.not_signed_in", "Not signed in") }}
73124 </template >
74- <template v-else >Not signed in</template >
75125 </p >
126+ <!-- The panel's own toast, reached through the `notify` prop. -->
127+ <button
128+ class =" rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
129+ @click =" sayHi()"
130+ >
131+ Toast me
132+ </button >
76133 <p class =" max-w-md text-sm text-muted-foreground" >
77134 This whole page is a Module Federation remote rendered natively inside
78135 the 5stack panel — same nav, theme, and login.
0 commit comments