glashjs 0.13.5 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/assets/animated-favicon.mjs +6 -7
- package/src/build.mjs +3 -3
- package/src/config.mjs +2 -2
- package/src/create.mjs +73 -4
- package/src/i18n.mjs +185 -0
- package/src/server/html.mjs +1 -1
- package/templates/glash_favicon.svg +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glashjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "glashjs — The Postgres-native full-stack framework for builders who want to ship without DevOps. Framework, hosting, database, auth, and deploy in one GlashDB-native runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"./image": "./src/components/image.mjs",
|
|
22
22
|
"./video": "./src/components/video.mjs",
|
|
23
23
|
"./link": "./src/components/link.mjs",
|
|
24
|
-
"./package.json": "./package.json"
|
|
24
|
+
"./package.json": "./package.json",
|
|
25
|
+
"./i18n": "./src/i18n.mjs"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"bin",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// glashjs animated favicon
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
|
-
// Ships an animated
|
|
4
|
-
// -
|
|
5
|
-
// tab icon natively (the default glash mark).
|
|
3
|
+
// Ships an optional animated/favicon runtime with every build. Two reliable modes:
|
|
4
|
+
// - SVG -> set once; the default is the official glash mark.
|
|
6
5
|
// - frame cycling -> a tiny runtime swaps the <link rel=icon> href
|
|
7
6
|
// across frames on an interval. Works everywhere,
|
|
8
7
|
// and pauses while the tab is hidden (no wasted CPU).
|
|
@@ -47,7 +46,7 @@ if (CONFIG.auto) startGlashFavicon();
|
|
|
47
46
|
/**
|
|
48
47
|
* Resolve + emit the animated favicon for a build.
|
|
49
48
|
* cfg.animatedFavicon may be:
|
|
50
|
-
* true -> the bundled default glash
|
|
49
|
+
* true -> the bundled default glash mark
|
|
51
50
|
* "<path>" -> a project animated SVG/GIF
|
|
52
51
|
* { frames:[...], fps, auto } -> frame-cycling animation
|
|
53
52
|
*/
|
|
@@ -59,14 +58,14 @@ export async function generateAnimatedFavicon(outDir, cfg, root, log = () => {})
|
|
|
59
58
|
|
|
60
59
|
if (setting === true || typeof setting === 'string') {
|
|
61
60
|
const src = setting === true
|
|
62
|
-
? fileURLToPath(new URL('../../templates/
|
|
61
|
+
? fileURLToPath(new URL('../../templates/glash_favicon.svg', import.meta.url))
|
|
63
62
|
: path.resolve(root, setting);
|
|
64
63
|
const out = 'favicon-animated' + path.extname(src);
|
|
65
64
|
try {
|
|
66
65
|
await fs.copyFile(src, path.join(outDir, out));
|
|
67
66
|
} catch {
|
|
68
|
-
log(` ! animated favicon source not found (${setting}) — using bundled
|
|
69
|
-
await fs.copyFile(fileURLToPath(new URL('../../templates/
|
|
67
|
+
log(` ! animated favicon source not found (${setting}) — using bundled glash_favicon.svg`);
|
|
68
|
+
await fs.copyFile(fileURLToPath(new URL('../../templates/glash_favicon.svg', import.meta.url)), path.join(outDir, 'favicon-animated.svg'));
|
|
70
69
|
baked.animated = '/favicon-animated.svg';
|
|
71
70
|
}
|
|
72
71
|
baked.animated = baked.animated || '/' + out;
|
package/src/build.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
|
|
|
115
115
|
log(` saved ${t.savedPercent}% (${kb(t.originalBytes - t.optimizedBytes)})`);
|
|
116
116
|
log(` routes ${routesBuilt.compiled} JSX route(s) precompiled (no runtime esbuild in prod)`);
|
|
117
117
|
log(` offline ${cfg.offline ? `${offline.precached} files precached (works offline after first visit)` : 'disabled'}`);
|
|
118
|
-
log(` favicon
|
|
118
|
+
log(` favicon glash_favicon.svg${animated.enabled ? ' + animated (glash-favicon.mjs)' : ''}`);
|
|
119
119
|
log(` version ${version}`);
|
|
120
120
|
log(` security strict CSP + ${Object.keys(securityHeaders(cfg.security)).length} headers\n`);
|
|
121
121
|
|
|
@@ -126,7 +126,7 @@ export async function build({ root = process.cwd(), log = console.log } = {}) {
|
|
|
126
126
|
// framework. Try the configured path first, then fall back to that bundled logo
|
|
127
127
|
// so the glashdb favicon shows out-of-the-box even before any override.
|
|
128
128
|
async function copyFavicon(cfg, root, outDir, log) {
|
|
129
|
-
const bundled = path.resolve(fileURLToPath(new URL('../templates/
|
|
129
|
+
const bundled = path.resolve(fileURLToPath(new URL('../templates/glash_favicon.svg', import.meta.url)));
|
|
130
130
|
const candidates = [path.resolve(root, cfg.favicon), bundled];
|
|
131
131
|
for (const src of candidates) {
|
|
132
132
|
try {
|
|
@@ -134,7 +134,7 @@ async function copyFavicon(cfg, root, outDir, log) {
|
|
|
134
134
|
return;
|
|
135
135
|
} catch { /* try next */ }
|
|
136
136
|
}
|
|
137
|
-
log(` ! favicon not found (looked in ${cfg.favicon}, bundled
|
|
137
|
+
log(` ! favicon not found (looked in ${cfg.favicon}, bundled glash_favicon.svg) — preview favicon will be missing`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function kb(bytes) {
|
package/src/config.mjs
CHANGED
|
@@ -11,11 +11,11 @@ export const DEFAULT_CONFIG = {
|
|
|
11
11
|
shortName: undefined,
|
|
12
12
|
// The preview favicon defaults to the official glashdb logo shipped with the
|
|
13
13
|
// framework; override with your own path relative to the project root.
|
|
14
|
-
favicon: 'node_modules/glashjs/templates/
|
|
14
|
+
favicon: 'node_modules/glashjs/templates/glash_favicon.svg',
|
|
15
15
|
// Animated favicon: true = bundled animated glash mark, a path to your own
|
|
16
16
|
// animated SVG/GIF, or { frames: ['/f0.svg', ...], fps: 8 } to cycle frames.
|
|
17
17
|
// false disables it. Emits `glash-favicon.mjs` — call startGlashFavicon() once.
|
|
18
|
-
animatedFavicon:
|
|
18
|
+
animatedFavicon: false,
|
|
19
19
|
publicDir: 'public',
|
|
20
20
|
// File-based routes (pages + api/) served by `glash dev` / `glash serve`.
|
|
21
21
|
routesDir: 'routes',
|
package/src/create.mjs
CHANGED
|
@@ -42,6 +42,8 @@ async function collectAnswers(options, rl) {
|
|
|
42
42
|
const auth = boolAnswer(options.auth, await ask(rl, 'Add glashAuth routes?', 'yes'));
|
|
43
43
|
const sqlRunner = boolAnswer(options.sqlRunner, await ask(rl, 'Add SQL runner support?', 'yes'));
|
|
44
44
|
const aiPrompts = boolAnswer(options.aiPrompts, await ask(rl, 'Add AI deployment prompts?', 'yes'));
|
|
45
|
+
const translate = boolAnswer(options.translate, await ask(rl, 'Add built-in IP auto-translation?', 'yes'));
|
|
46
|
+
const animation = boolAnswer(options.animation, await ask(rl, 'Add motion + 3D (motion, three.js)?', 'no'));
|
|
45
47
|
const install = boolAnswer(options.install, await ask(rl, 'Install dependencies now?', 'yes'));
|
|
46
48
|
const packageManager = normalizeChoice(options.packageManager || await ask(rl, 'Package manager (npm/pnpm/yarn/bun)', detectPackageManager()), PM_CHOICES, 'npm');
|
|
47
49
|
const git = boolAnswer(options.git, await ask(rl, 'Initialize git?', 'yes'));
|
|
@@ -54,6 +56,8 @@ async function collectAnswers(options, rl) {
|
|
|
54
56
|
auth,
|
|
55
57
|
sqlRunner,
|
|
56
58
|
aiPrompts,
|
|
59
|
+
translate,
|
|
60
|
+
animation,
|
|
57
61
|
install,
|
|
58
62
|
packageManager,
|
|
59
63
|
git,
|
|
@@ -85,6 +89,8 @@ function projectFiles(answers) {
|
|
|
85
89
|
pg: '^8.16.3',
|
|
86
90
|
preact: '^10.29.2',
|
|
87
91
|
'preact-render-to-string': '^6.7.0',
|
|
92
|
+
// Motion (animation) + three.js (3D), bundled client-side via esbuild.
|
|
93
|
+
...(answers.animation ? { motion: '^11.15.0', three: '^0.171.0' } : {}),
|
|
88
94
|
};
|
|
89
95
|
const devDependencies = answers.css === 'tailwind'
|
|
90
96
|
? { tailwindcss: '^4.1.0', '@tailwindcss/cli': '^4.1.0' }
|
|
@@ -105,7 +111,7 @@ function projectFiles(answers) {
|
|
|
105
111
|
'.env.local': envLocal(answers),
|
|
106
112
|
'glash.config.mjs': configFile(answers, hasCss),
|
|
107
113
|
'README.md': projectReadme(answers),
|
|
108
|
-
'routes/_layout.jsx': layoutRoute(),
|
|
114
|
+
'routes/_layout.jsx': layoutRoute(answers),
|
|
109
115
|
'routes/index.jsx': indexRoute(answers),
|
|
110
116
|
'routes/api/health.mjs': healthRoute(),
|
|
111
117
|
'db/schema.sql': schemaSql(answers),
|
|
@@ -113,6 +119,8 @@ function projectFiles(answers) {
|
|
|
113
119
|
...(answers.css === 'tailwind' ? { 'styles/input.css': tailwindInput() } : {}),
|
|
114
120
|
...(answers.auth ? authRoutes() : {}),
|
|
115
121
|
...(answers.sqlRunner ? sqlRunnerRoutes() : {}),
|
|
122
|
+
...(answers.translate ? { 'routes/api/_glash/geo.mjs': geoRoute() } : {}),
|
|
123
|
+
...(answers.animation ? { 'routes/demo/motion.jsx': motionDemoRoute() } : {}),
|
|
116
124
|
...(answers.aiPrompts ? { '.glash/prompts/deploy.md': aiDeployPrompt(answers) } : {}),
|
|
117
125
|
'public/favicon.svg': faviconSvg(),
|
|
118
126
|
};
|
|
@@ -140,10 +148,19 @@ export default defineConfig({
|
|
|
140
148
|
`;
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
function layoutRoute() {
|
|
144
|
-
|
|
151
|
+
function layoutRoute(answers = {}) {
|
|
152
|
+
const t = answers.translate;
|
|
153
|
+
const imports = [`import { Link } from 'glashjs/link';`];
|
|
154
|
+
if (t) {
|
|
155
|
+
imports.push(`import { useEffect } from 'preact/hooks';`);
|
|
156
|
+
imports.push(`import { glashAutoTranslate } from 'glashjs/i18n';`);
|
|
157
|
+
}
|
|
158
|
+
return `${imports.join('\n')}
|
|
145
159
|
|
|
146
|
-
export default function RootLayout({ children }) {
|
|
160
|
+
export default function RootLayout({ children }) {${t ? `
|
|
161
|
+
// Built-in IP auto-translation: detect the visitor's country and offer to
|
|
162
|
+
// translate the page into their native language. Reads /api/_glash/geo.
|
|
163
|
+
useEffect(() => { glashAutoTranslate(); }, []);` : ''}
|
|
147
164
|
return (
|
|
148
165
|
<div className="shell">
|
|
149
166
|
<header className="nav">
|
|
@@ -160,6 +177,58 @@ export default function RootLayout({ children }) {
|
|
|
160
177
|
`;
|
|
161
178
|
}
|
|
162
179
|
|
|
180
|
+
function geoRoute() {
|
|
181
|
+
return `// IP -> country -> native language, read from the edge geo header
|
|
182
|
+
// (CF-IPCountry / X-Glash-Country on glashDB hosting). The client i18n runtime
|
|
183
|
+
// calls this to decide whether to offer auto-translation.
|
|
184
|
+
import { geoRouteHandler } from 'glashjs/i18n';
|
|
185
|
+
|
|
186
|
+
export const GET = geoRouteHandler;
|
|
187
|
+
`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function motionDemoRoute() {
|
|
191
|
+
return `import { useEffect, useRef } from 'preact/hooks';
|
|
192
|
+
import { animate } from 'motion';
|
|
193
|
+
import * as THREE from 'three';
|
|
194
|
+
|
|
195
|
+
export const metadata = { title: 'Motion + 3D' };
|
|
196
|
+
|
|
197
|
+
export default function MotionDemo() {
|
|
198
|
+
const card = useRef(null);
|
|
199
|
+
const canvas = useRef(null);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (card.current) {
|
|
203
|
+
animate(card.current, { rotate: [0, 8, -8, 0], scale: [1, 1.06, 1] }, { duration: 2.4, repeat: Infinity });
|
|
204
|
+
}
|
|
205
|
+
const el = canvas.current;
|
|
206
|
+
if (!el) return;
|
|
207
|
+
const renderer = new THREE.WebGLRenderer({ canvas: el, antialias: true, alpha: true });
|
|
208
|
+
renderer.setSize(240, 240, false);
|
|
209
|
+
const scene = new THREE.Scene();
|
|
210
|
+
const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
|
|
211
|
+
camera.position.z = 3;
|
|
212
|
+
const cube = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshNormalMaterial());
|
|
213
|
+
scene.add(cube);
|
|
214
|
+
let raf;
|
|
215
|
+
const loop = () => { cube.rotation.x += 0.01; cube.rotation.y += 0.013; renderer.render(scene, camera); raf = requestAnimationFrame(loop); };
|
|
216
|
+
loop();
|
|
217
|
+
return () => cancelAnimationFrame(raf);
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<main className="hero">
|
|
222
|
+
<h1>Motion + 3D</h1>
|
|
223
|
+
<p className="lede">motion drives the card animation; three.js renders the cube.</p>
|
|
224
|
+
<div ref={card} style={{ width: '120px', height: '120px', borderRadius: '16px', background: '#e8eaed' }} />
|
|
225
|
+
<canvas ref={canvas} width={240} height={240} />
|
|
226
|
+
</main>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
163
232
|
function indexRoute(answers) {
|
|
164
233
|
return `import { useState } from 'preact/hooks';
|
|
165
234
|
import { callServerFunction } from 'glashjs/server-functions';
|
package/src/i18n.mjs
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// glashjs/i18n — built-in IP-based auto-translation.
|
|
2
|
+
//
|
|
3
|
+
// Server:
|
|
4
|
+
// detectCountry(req) -> 'US' | null (reads edge/CDN geo headers)
|
|
5
|
+
// languageForCountry('FR') -> { code: 'fr', name: 'Français' } | null
|
|
6
|
+
//
|
|
7
|
+
// Client:
|
|
8
|
+
// glashAutoTranslate(opts) -> looks up the visitor's country (via a glashjs
|
|
9
|
+
// geo API route, falling back to a public IP service), maps it to a language,
|
|
10
|
+
// and — if that differs from the page language — shows a small prompt to
|
|
11
|
+
// translate the page into the visitor's native language. No API key: it uses
|
|
12
|
+
// the Google website-translate widget (cookie-driven, in-place).
|
|
13
|
+
//
|
|
14
|
+
// Wire it up in a client effect:
|
|
15
|
+
// import { glashAutoTranslate } from 'glashjs/i18n';
|
|
16
|
+
// useEffect(() => glashAutoTranslate(), []);
|
|
17
|
+
// and scaffold `routes/api/_glash/geo.mjs` (see geoRouteHandler) so detection
|
|
18
|
+
// works on glashDB hosting, where the edge sets the country header.
|
|
19
|
+
|
|
20
|
+
// country code -> [BCP-47 language, native name]
|
|
21
|
+
export const COUNTRY_LANGUAGE = {
|
|
22
|
+
US: ['en', 'English'], GB: ['en', 'English'], AU: ['en', 'English'], CA: ['en', 'English'],
|
|
23
|
+
IE: ['en', 'English'], NZ: ['en', 'English'], NG: ['en', 'English'], GH: ['en', 'English'],
|
|
24
|
+
KE: ['en', 'English'], ZA: ['en', 'English'], IN: ['hi', 'हिन्दी'], PK: ['ur', 'اردو'],
|
|
25
|
+
FR: ['fr', 'Français'], BE: ['fr', 'Français'], LU: ['fr', 'Français'], CI: ['fr', 'Français'],
|
|
26
|
+
SN: ['fr', 'Français'], CM: ['fr', 'Français'], ES: ['es', 'Español'], MX: ['es', 'Español'],
|
|
27
|
+
AR: ['es', 'Español'], CO: ['es', 'Español'], CL: ['es', 'Español'], PE: ['es', 'Español'],
|
|
28
|
+
VE: ['es', 'Español'], DE: ['de', 'Deutsch'], AT: ['de', 'Deutsch'], CH: ['de', 'Deutsch'],
|
|
29
|
+
IT: ['it', 'Italiano'], PT: ['pt', 'Português'], BR: ['pt', 'Português'], AO: ['pt', 'Português'],
|
|
30
|
+
NL: ['nl', 'Nederlands'], RU: ['ru', 'Русский'], UA: ['uk', 'Українська'], PL: ['pl', 'Polski'],
|
|
31
|
+
CZ: ['cs', 'Čeština'], SK: ['sk', 'Slovenčina'], RO: ['ro', 'Română'], HU: ['hu', 'Magyar'],
|
|
32
|
+
GR: ['el', 'Ελληνικά'], TR: ['tr', 'Türkçe'], SE: ['sv', 'Svenska'], NO: ['no', 'Norsk'],
|
|
33
|
+
DK: ['da', 'Dansk'], FI: ['fi', 'Suomi'], IS: ['is', 'Íslenska'], CN: ['zh-CN', '中文'],
|
|
34
|
+
TW: ['zh-TW', '繁體中文'], HK: ['zh-TW', '繁體中文'], JP: ['ja', '日本語'], KR: ['ko', '한국어'],
|
|
35
|
+
TH: ['th', 'ไทย'], VN: ['vi', 'Tiếng Việt'], ID: ['id', 'Bahasa Indonesia'], MY: ['ms', 'Bahasa Melayu'],
|
|
36
|
+
PH: ['tl', 'Filipino'], SA: ['ar', 'العربية'], AE: ['ar', 'العربية'], EG: ['ar', 'العربية'],
|
|
37
|
+
MA: ['ar', 'العربية'], DZ: ['ar', 'العربية'], IQ: ['ar', 'العربية'], IL: ['he', 'עברית'],
|
|
38
|
+
IR: ['fa', 'فارسی'], BD: ['bn', 'বাংলা'], ET: ['am', 'አማርኛ'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function detectCountry(req) {
|
|
42
|
+
const h = (req && req.headers) || {};
|
|
43
|
+
const get = (k) => (typeof h.get === 'function' ? h.get(k) : (h[k] ?? h[k.toLowerCase()]));
|
|
44
|
+
const cc = String(
|
|
45
|
+
get('cf-ipcountry') || get('x-glash-country') || get('x-vercel-ip-country') || get('x-country-code') || '',
|
|
46
|
+
).split(',')[0].trim().toUpperCase();
|
|
47
|
+
return /^[A-Z]{2}$/.test(cc) && cc !== 'XX' ? cc : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function languageForCountry(country) {
|
|
51
|
+
const entry = COUNTRY_LANGUAGE[String(country || '').toUpperCase()];
|
|
52
|
+
return entry ? { code: entry[0], name: entry[1] } : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Drop-in handler for `routes/api/_glash/geo.mjs` (export const GET = geoRouteHandler). */
|
|
56
|
+
export function geoRouteHandler(ctx) {
|
|
57
|
+
const country = detectCountry({ headers: (ctx && ctx.headers) || {} });
|
|
58
|
+
const language = country ? languageForCountry(country) : null;
|
|
59
|
+
return { country, language };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- client ---------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function baseLang(code) { return String(code || '').toLowerCase().split('-')[0]; }
|
|
65
|
+
|
|
66
|
+
async function lookupLanguage(geoEndpoint) {
|
|
67
|
+
// 1) glashjs geo route (works on glashDB hosting via the edge country header).
|
|
68
|
+
try {
|
|
69
|
+
const r = await fetch(geoEndpoint, { headers: { accept: 'application/json' } });
|
|
70
|
+
if (r.ok) {
|
|
71
|
+
const d = await r.json();
|
|
72
|
+
if (d && d.language && d.language.code) return d.language;
|
|
73
|
+
if (d && d.country) { const l = languageForCountry(d.country); if (l) return l; }
|
|
74
|
+
}
|
|
75
|
+
} catch { /* fall through */ }
|
|
76
|
+
// 2) Public IP geolocation fallback (no key, CORS-enabled).
|
|
77
|
+
try {
|
|
78
|
+
const r = await fetch('https://ipapi.co/json/');
|
|
79
|
+
if (r.ok) {
|
|
80
|
+
const d = await r.json();
|
|
81
|
+
const l = d && d.country_code ? languageForCountry(d.country_code) : null;
|
|
82
|
+
if (l) return l;
|
|
83
|
+
}
|
|
84
|
+
} catch { /* fall through */ }
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setGoogTransCookie(from, to) {
|
|
89
|
+
const val = `/${from}/${to}`;
|
|
90
|
+
const host = location.hostname;
|
|
91
|
+
document.cookie = `googtrans=${val};path=/`;
|
|
92
|
+
document.cookie = `googtrans=${val};path=/;domain=${host}`;
|
|
93
|
+
if (host.split('.').length > 1) document.cookie = `googtrans=${val};path=/;domain=.${host}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadGoogleWidget(pageLanguage) {
|
|
97
|
+
if (window.__glashGTLoaded) return;
|
|
98
|
+
window.__glashGTLoaded = true;
|
|
99
|
+
if (!document.getElementById('glash-gt')) {
|
|
100
|
+
const el = document.createElement('div');
|
|
101
|
+
el.id = 'glash-gt';
|
|
102
|
+
el.style.display = 'none';
|
|
103
|
+
document.body.appendChild(el);
|
|
104
|
+
}
|
|
105
|
+
window.googleTranslateElementInit = function () {
|
|
106
|
+
try { new window.google.translate.TranslateElement({ pageLanguage, autoDisplay: false }, 'glash-gt'); } catch { /* ignore */ }
|
|
107
|
+
};
|
|
108
|
+
const s = document.createElement('script');
|
|
109
|
+
s.src = 'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
|
|
110
|
+
document.head.appendChild(s);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function showPrompt(language, onAccept, onDismiss) {
|
|
114
|
+
if (document.getElementById('glash-translate-prompt')) return;
|
|
115
|
+
const box = document.createElement('div');
|
|
116
|
+
box.id = 'glash-translate-prompt';
|
|
117
|
+
box.setAttribute('role', 'dialog');
|
|
118
|
+
box.style.cssText = [
|
|
119
|
+
'position:fixed', 'z-index:2147483646', 'left:50%', 'bottom:20px', 'transform:translateX(-50%)',
|
|
120
|
+
'max-width:min(92vw,460px)', 'display:flex', 'gap:12px', 'align-items:center',
|
|
121
|
+
'padding:12px 14px', 'border-radius:12px', 'font:14px/1.4 system-ui,sans-serif',
|
|
122
|
+
'background:#0b0d12', 'color:#e8eaed', 'border:1px solid rgba(255,255,255,0.12)',
|
|
123
|
+
'box-shadow:0 8px 30px rgba(0,0,0,0.45)',
|
|
124
|
+
].join(';');
|
|
125
|
+
const text = document.createElement('span');
|
|
126
|
+
text.style.cssText = 'flex:1';
|
|
127
|
+
text.textContent = `View this site in ${language.name}?`;
|
|
128
|
+
const yes = document.createElement('button');
|
|
129
|
+
yes.textContent = 'Translate';
|
|
130
|
+
yes.style.cssText = 'cursor:pointer;border:0;border-radius:8px;padding:7px 12px;font:inherit;background:#e8eaed;color:#0b0d12;font-weight:600';
|
|
131
|
+
const no = document.createElement('button');
|
|
132
|
+
no.textContent = 'No thanks';
|
|
133
|
+
no.style.cssText = 'cursor:pointer;border:0;border-radius:8px;padding:7px 10px;font:inherit;background:transparent;color:#9aa0a6';
|
|
134
|
+
yes.addEventListener('click', () => { box.remove(); onAccept(); });
|
|
135
|
+
no.addEventListener('click', () => { box.remove(); onDismiss(); });
|
|
136
|
+
box.append(text, yes, no);
|
|
137
|
+
document.body.appendChild(box);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Detect the visitor's country by IP, map it to their native language, and (if it
|
|
142
|
+
* differs from the page language) prompt to auto-translate the page in place.
|
|
143
|
+
*
|
|
144
|
+
* Options:
|
|
145
|
+
* pageLanguage language the site is authored in (default: <html lang> or 'en')
|
|
146
|
+
* geoEndpoint glashjs geo route (default: '/api/_glash/geo')
|
|
147
|
+
* auto translate immediately without prompting (default: false)
|
|
148
|
+
* storageKey localStorage key remembering the choice
|
|
149
|
+
*/
|
|
150
|
+
export function glashAutoTranslate(opts = {}) {
|
|
151
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
152
|
+
const {
|
|
153
|
+
pageLanguage = baseLang(document.documentElement.lang) || 'en',
|
|
154
|
+
geoEndpoint = '/api/_glash/geo',
|
|
155
|
+
auto = false,
|
|
156
|
+
storageKey = 'glash:autotranslate',
|
|
157
|
+
} = opts;
|
|
158
|
+
|
|
159
|
+
// If a previous choice already translated the page, re-apply the widget so the
|
|
160
|
+
// translation persists across navigations, then stop.
|
|
161
|
+
if (/(^|;\s*)googtrans=\//.test(document.cookie)) { loadGoogleWidget(pageLanguage); return; }
|
|
162
|
+
let saved = null;
|
|
163
|
+
try { saved = localStorage.getItem(storageKey); } catch { /* ignore */ }
|
|
164
|
+
if (saved === 'dismissed') return;
|
|
165
|
+
|
|
166
|
+
const translateTo = (code) => {
|
|
167
|
+
setGoogTransCookie(pageLanguage, code);
|
|
168
|
+
loadGoogleWidget(pageLanguage);
|
|
169
|
+
setTimeout(() => location.reload(), 60);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (saved && saved !== pageLanguage) { translateTo(saved); return; }
|
|
173
|
+
|
|
174
|
+
lookupLanguage(geoEndpoint).then((language) => {
|
|
175
|
+
if (!language || baseLang(language.code) === pageLanguage) return;
|
|
176
|
+
if (auto) { try { localStorage.setItem(storageKey, language.code); } catch {} translateTo(language.code); return; }
|
|
177
|
+
showPrompt(
|
|
178
|
+
language,
|
|
179
|
+
() => { try { localStorage.setItem(storageKey, language.code); } catch {} translateTo(language.code); },
|
|
180
|
+
() => { try { localStorage.setItem(storageKey, 'dismissed'); } catch {} },
|
|
181
|
+
);
|
|
182
|
+
}).catch(() => {});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default { detectCountry, languageForCountry, geoRouteHandler, glashAutoTranslate, COUNTRY_LANGUAGE };
|
package/src/server/html.mjs
CHANGED
|
@@ -49,7 +49,7 @@ ${headHtml}
|
|
|
49
49
|
`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function shellTail({ offline = true, animatedFavicon =
|
|
52
|
+
function shellTail({ offline = true, animatedFavicon = false, dev = false, nonce = '' }) {
|
|
53
53
|
const n = nonce ? ` nonce="${escapeHtml(nonce)}"` : '';
|
|
54
54
|
const fav = animatedFavicon
|
|
55
55
|
? `<script type="module"${n}>try{const m=await import("/glash-favicon.mjs");m.startGlashFavicon&&m.startGlashFavicon();}catch{}</script>`
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<svg width="508" height="508" viewBox="0 0 508 508" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g filter="url(#filter0_ii_12189_2)">
|
|
3
|
+
<rect x="43.9863" y="34.4604" width="420.999" height="420.999" rx="102.477" fill="url(#paint0_linear_12189_2)"/>
|
|
4
|
+
</g>
|
|
5
|
+
<rect x="45.1251" y="35.5992" width="418.722" height="418.722" rx="101.338" stroke="#D4D4D4" stroke-width="2.27759"/>
|
|
6
|
+
<g opacity="0.6" filter="url(#filter1_f_12189_2)" style="mix-blend-mode:overlay">
|
|
7
|
+
<circle cx="253.556" cy="253.556" r="119.602" fill="url(#paint1_linear_12189_2)"/>
|
|
8
|
+
<circle cx="253.556" cy="253.556" r="118.463" stroke="#D4D4D4" stroke-width="2.27759"/>
|
|
9
|
+
</g>
|
|
10
|
+
<mask id="mask0_12189_2" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="43" y="34" width="422" height="422">
|
|
11
|
+
<rect x="45.1241" y="35.5997" width="418.722" height="418.722" rx="101.338" fill="url(#paint2_linear_12189_2)" stroke="#D4D4D4" stroke-width="2.27759"/>
|
|
12
|
+
</mask>
|
|
13
|
+
<g mask="url(#mask0_12189_2)">
|
|
14
|
+
<g filter="url(#filter2_f_12189_2)">
|
|
15
|
+
<ellipse cx="259.269" cy="23.0167" rx="215.284" ry="122.507" fill="url(#paint3_linear_12189_2)"/>
|
|
16
|
+
<path d="M259.269 -98.3521C318.557 -98.3521 372.177 -84.6747 410.935 -62.6196C449.725 -40.5457 473.414 -10.2249 473.414 23.0171C473.414 56.2589 449.725 86.579 410.935 108.653C372.177 130.708 318.557 144.385 259.269 144.385C199.98 144.385 146.361 130.708 107.604 108.653C68.813 86.579 45.1242 56.2589 45.124 23.0171C45.124 -10.2249 68.8128 -40.5457 107.604 -62.6196C146.361 -84.6747 199.98 -98.352 259.269 -98.3521Z" stroke="#D4D4D4" stroke-width="2.27759"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g filter="url(#filter3_f_12189_2)" style="mix-blend-mode:plus-lighter">
|
|
19
|
+
<ellipse cx="254.486" cy="29.677" rx="47.8408" ry="81.3294" fill="url(#paint4_linear_12189_2)"/>
|
|
20
|
+
<path d="M254.486 -50.5137C267.129 -50.5137 278.78 -41.7943 287.333 -27.2539C295.871 -12.7399 301.188 7.3854 301.188 29.6768C301.188 51.9682 295.871 72.0943 287.333 86.6084C278.78 101.149 267.129 109.867 254.486 109.867C241.844 109.867 230.193 101.149 221.64 86.6084C213.102 72.0943 207.784 51.9682 207.784 29.6768C207.784 7.3854 213.102 -12.7399 221.64 -27.2539C230.193 -41.7943 241.844 -50.5137 254.486 -50.5137Z" stroke="#D4D4D4" stroke-width="2.27759"/>
|
|
21
|
+
</g>
|
|
22
|
+
</g>
|
|
23
|
+
<path d="M336.671 312.331L239.265 374.956L164.548 342.247L261.906 279.621L336.671 312.331Z" fill="#FEFFFF"/>
|
|
24
|
+
<path d="M264.17 166.655C286.185 166.655 304.057 184.575 304.057 206.591C304.057 213.479 302.371 219.983 299.047 225.956C292.014 238.626 278.67 246.478 264.17 246.478C258.148 246.478 252.367 245.178 246.972 242.624C233.146 236.025 224.234 221.91 224.234 206.591C224.234 184.575 242.155 166.655 264.17 166.655ZM264.17 128.116C220.814 128.116 185.695 163.235 185.695 206.591C185.695 237.807 203.953 264.784 230.352 277.406C240.565 282.319 252.03 285.017 264.122 285.017C293.604 285.017 319.328 268.734 332.672 244.648C338.983 233.375 342.548 220.416 342.548 206.591C342.548 163.283 307.43 128.116 264.122 128.116H264.17Z" fill="#FEFFFF"/>
|
|
25
|
+
<defs>
|
|
26
|
+
<filter id="filter0_ii_12189_2" x="43.9863" y="34.4604" width="420.999" height="449.704" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
27
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
28
|
+
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
29
|
+
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
30
|
+
<feOffset/>
|
|
31
|
+
<feGaussianBlur stdDeviation="19.1363"/>
|
|
32
|
+
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
|
33
|
+
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.24 0"/>
|
|
34
|
+
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_12189_2"/>
|
|
35
|
+
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
36
|
+
<feOffset dy="28.7045"/>
|
|
37
|
+
<feGaussianBlur stdDeviation="19.1363"/>
|
|
38
|
+
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
|
39
|
+
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0"/>
|
|
40
|
+
<feBlend mode="normal" in2="effect1_innerShadow_12189_2" result="effect2_innerShadow_12189_2"/>
|
|
41
|
+
</filter>
|
|
42
|
+
<filter id="filter1_f_12189_2" x="-0.000152588" y="-0.000152588" width="507.113" height="507.113" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
43
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
44
|
+
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
45
|
+
<feGaussianBlur stdDeviation="66.9771" result="effect1_foregroundBlur_12189_2"/>
|
|
46
|
+
</filter>
|
|
47
|
+
<filter id="filter2_f_12189_2" x="-109.105" y="-252.581" width="736.749" height="551.196" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
48
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
49
|
+
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
50
|
+
<feGaussianBlur stdDeviation="76.5453" result="effect1_foregroundBlur_12189_2"/>
|
|
51
|
+
</filter>
|
|
52
|
+
<filter id="filter3_f_12189_2" x="149.237" y="-109.061" width="210.5" height="277.477" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
53
|
+
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
54
|
+
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
55
|
+
<feGaussianBlur stdDeviation="28.7045" result="effect1_foregroundBlur_12189_2"/>
|
|
56
|
+
</filter>
|
|
57
|
+
<linearGradient id="paint0_linear_12189_2" x1="254.486" y1="34.4604" x2="254.486" y2="455.46" gradientUnits="userSpaceOnUse">
|
|
58
|
+
<stop stop-opacity="0.3"/>
|
|
59
|
+
<stop offset="1" stop-opacity="0.6"/>
|
|
60
|
+
</linearGradient>
|
|
61
|
+
<linearGradient id="paint1_linear_12189_2" x1="253.556" y1="133.954" x2="253.556" y2="373.158" gradientUnits="userSpaceOnUse">
|
|
62
|
+
<stop stop-opacity="0"/>
|
|
63
|
+
<stop offset="1" stop-opacity="0.6"/>
|
|
64
|
+
</linearGradient>
|
|
65
|
+
<linearGradient id="paint2_linear_12189_2" x1="254.485" y1="34.4609" x2="254.485" y2="455.46" gradientUnits="userSpaceOnUse">
|
|
66
|
+
<stop stop-color="#4B4B4B"/>
|
|
67
|
+
<stop offset="1" stop-color="#AFAFAF"/>
|
|
68
|
+
</linearGradient>
|
|
69
|
+
<linearGradient id="paint3_linear_12189_2" x1="272.501" y1="142.813" x2="267.703" y2="-48.6569" gradientUnits="userSpaceOnUse">
|
|
70
|
+
<stop stop-color="#D8D8D8" stop-opacity="0.05"/>
|
|
71
|
+
<stop offset="0.61" stop-color="#B2B2B2" stop-opacity="0.05"/>
|
|
72
|
+
<stop offset="0.98" stop-color="#404040"/>
|
|
73
|
+
</linearGradient>
|
|
74
|
+
<linearGradient id="paint4_linear_12189_2" x1="254.486" y1="-51.6523" x2="254.486" y2="111.006" gradientUnits="userSpaceOnUse">
|
|
75
|
+
<stop stop-opacity="0"/>
|
|
76
|
+
<stop offset="1" stop-opacity="0.6"/>
|
|
77
|
+
</linearGradient>
|
|
78
|
+
</defs>
|
|
79
|
+
</svg>
|