@zachhandley/ez-i18n 0.2.2 → 0.3.0

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@zachhandley/ez-i18n",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Cookie-based i18n for Astro + Vue + React. No URL prefixes, reactive language switching.",
7
+ "description": "Cookie-based i18n for Astro. Core package with Astro integration and runtime.",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -13,10 +13,6 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
15
  },
16
- "./vue": {
17
- "types": "./dist/runtime/vue-plugin.d.ts",
18
- "import": "./dist/runtime/vue-plugin.js"
19
- },
20
16
  "./middleware": {
21
17
  "import": "./dist/middleware.js"
22
18
  },
@@ -27,10 +23,6 @@
27
23
  "./astro": {
28
24
  "import": "./src/components/EzI18nHead.astro"
29
25
  },
30
- "./react": {
31
- "types": "./dist/runtime/react-plugin.d.ts",
32
- "import": "./dist/runtime/react-plugin.js"
33
- },
34
26
  "./utils": {
35
27
  "types": "./dist/utils/index.d.ts",
36
28
  "import": "./dist/utils/index.js"
@@ -40,21 +32,11 @@
40
32
  "dist",
41
33
  "src"
42
34
  ],
43
- "scripts": {
44
- "build": "tsup",
45
- "dev": "tsup --watch",
46
- "typecheck": "tsc --noEmit",
47
- "prepublishOnly": "pnpm build",
48
- "release": "pnpm build && npm publish",
49
- "release:dry": "pnpm build && npm publish --dry-run"
50
- },
51
35
  "keywords": [
52
36
  "astro",
53
37
  "astro-integration",
54
38
  "i18n",
55
39
  "internationalization",
56
- "vue",
57
- "react",
58
40
  "cookie-based",
59
41
  "no-url-prefix"
60
42
  ],
@@ -63,7 +45,8 @@
63
45
  "homepage": "https://github.com/zachhandley/ez-i18n#readme",
64
46
  "repository": {
65
47
  "type": "git",
66
- "url": "git+https://github.com/zachhandley/ez-i18n.git"
48
+ "url": "git+https://github.com/zachhandley/ez-i18n.git",
49
+ "directory": "packages/core"
67
50
  },
68
51
  "bugs": {
69
52
  "url": "https://github.com/zachhandley/ez-i18n/issues"
@@ -73,40 +56,22 @@
73
56
  },
74
57
  "peerDependencies": {
75
58
  "@nanostores/persistent": "^0.10.0",
76
- "@nanostores/react": "^0.7.0 || ^0.8.0 || ^1.0.0",
77
- "@nanostores/vue": "^0.10.0",
78
59
  "astro": "^4.0.0 || ^5.0.0",
79
- "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0",
80
- "react": "^18.0.0 || ^19.0.0",
81
- "vue": "^3.4.0"
82
- },
83
- "peerDependenciesMeta": {
84
- "vue": {
85
- "optional": true
86
- },
87
- "react": {
88
- "optional": true
89
- },
90
- "@nanostores/vue": {
91
- "optional": true
92
- },
93
- "@nanostores/react": {
94
- "optional": true
95
- }
60
+ "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0"
96
61
  },
97
62
  "devDependencies": {
98
63
  "@nanostores/persistent": "^0.10.2",
99
- "@nanostores/react": "^1.0.0",
100
- "@nanostores/vue": "^0.10.0",
101
64
  "@types/node": "^22.0.0",
102
- "@types/react": "^19.2.7",
103
65
  "astro": "^5.1.1",
104
66
  "nanostores": "^0.11.3",
105
- "react": "^19.2.1",
106
67
  "tsup": "^8.3.5",
107
68
  "typescript": "^5.7.2",
108
- "vite": "^6.0.3",
109
- "vue": "^3.5.13"
69
+ "vite": "^6.0.3"
110
70
  },
111
- "packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a"
112
- }
71
+ "scripts": {
72
+ "build": "tsup",
73
+ "dev": "tsup --watch",
74
+ "typecheck": "tsc --noEmit",
75
+ "clean": "rm -rf dist"
76
+ }
77
+ }
@@ -1,62 +1,62 @@
1
- ---
2
- /**
3
- * EzI18nHead - Astro component for i18n hydration setup
4
- *
5
- * Place this in your layout's <head> to properly initialize
6
- * the i18n runtime with server-side loaded translations.
7
- *
8
- * @example
9
- * ---
10
- * import EzI18nHead from '@zachhandley/ez-i18n/astro';
11
- * const { locale, translations } = Astro.locals;
12
- * ---
13
- * <html lang={locale}>
14
- * <head>
15
- * <EzI18nHead locale={locale} translations={translations} />
16
- * </head>
17
- * <body><slot /></body>
18
- * </html>
19
- */
20
- interface Props {
21
- /** Current locale code from Astro.locals.locale */
22
- locale: string;
23
- /** Translations object from Astro.locals.translations */
24
- translations: Record<string, unknown>;
25
- }
26
-
27
- const { locale, translations } = Astro.props;
28
-
29
- // Serialize translations for inline script
30
- const serializedTranslations = JSON.stringify(translations);
31
- ---
32
-
33
- <script
34
- is:inline
35
- data-ez-i18n-locale={locale}
36
- data-ez-i18n-translations={serializedTranslations}
37
- >
38
- // Initialize ez-i18n runtime with server-provided values
39
- (function() {
40
- const script = document.currentScript;
41
- if (!script) return;
42
-
43
- const locale = script.dataset.ezI18nLocale;
44
- const translations = JSON.parse(script.dataset.ezI18nTranslations || '{}');
45
-
46
- // Store for runtime initialization
47
- window.__EZ_I18N_INIT__ = { locale, translations };
48
- })();
49
- </script>
50
-
51
- <script>
52
- // Import and initialize the runtime stores
53
- import { initLocale, setTranslations } from '@zachhandley/ez-i18n/runtime';
54
-
55
- // Get initialization data from inline script
56
- const initData = (window as any).__EZ_I18N_INIT__;
57
- if (initData) {
58
- initLocale(initData.locale, initData.translations);
59
- setTranslations(initData.translations);
60
- delete (window as any).__EZ_I18N_INIT__;
61
- }
62
- </script>
1
+ ---
2
+ /**
3
+ * EzI18nHead - Astro component for i18n hydration setup
4
+ *
5
+ * Place this in your layout's <head> to properly initialize
6
+ * the i18n runtime with server-side loaded translations.
7
+ *
8
+ * @example
9
+ * ---
10
+ * import EzI18nHead from '@zachhandley/ez-i18n/astro';
11
+ * const { locale, translations } = Astro.locals;
12
+ * ---
13
+ * <html lang={locale}>
14
+ * <head>
15
+ * <EzI18nHead locale={locale} translations={translations} />
16
+ * </head>
17
+ * <body><slot /></body>
18
+ * </html>
19
+ */
20
+ interface Props {
21
+ /** Current locale code from Astro.locals.locale */
22
+ locale: string;
23
+ /** Translations object from Astro.locals.translations */
24
+ translations: Record<string, unknown>;
25
+ }
26
+
27
+ const { locale, translations } = Astro.props;
28
+
29
+ // Serialize translations for inline script
30
+ const serializedTranslations = JSON.stringify(translations);
31
+ ---
32
+
33
+ <script
34
+ is:inline
35
+ data-ez-i18n-locale={locale}
36
+ data-ez-i18n-translations={serializedTranslations}
37
+ >
38
+ // Initialize ez-i18n runtime with server-provided values
39
+ (function() {
40
+ const script = document.currentScript;
41
+ if (!script) return;
42
+
43
+ const locale = script.dataset.ezI18nLocale;
44
+ const translations = JSON.parse(script.dataset.ezI18nTranslations || '{}');
45
+
46
+ // Store for runtime initialization
47
+ window.__EZ_I18N_INIT__ = { locale, translations };
48
+ })();
49
+ </script>
50
+
51
+ <script>
52
+ // Import and initialize the runtime stores
53
+ import { initLocale, setTranslations } from '@zachhandley/ez-i18n/runtime';
54
+
55
+ // Get initialization data from inline script
56
+ const initData = (window as any).__EZ_I18N_INIT__;
57
+ if (initData) {
58
+ initLocale(initData.locale, initData.translations);
59
+ setTranslations(initData.translations);
60
+ delete (window as any).__EZ_I18N_INIT__;
61
+ }
62
+ </script>
package/src/index.ts CHANGED
@@ -1,122 +1,122 @@
1
- import type { AstroIntegration, HookParameters } from 'astro';
2
- import type { EzI18nConfig } from './types';
3
- import { vitePlugin, resolveConfig } from './vite-plugin';
4
-
5
- export type { EzI18nConfig, TranslateFunction } from './types';
6
-
7
- /**
8
- * ez-i18n Astro integration
9
- *
10
- * Provides cookie-based i18n without URL prefixes.
11
- *
12
- * @example
13
- * // astro.config.ts
14
- * import ezI18n from '@zachhandley/ez-i18n';
15
- *
16
- * export default defineConfig({
17
- * integrations: [
18
- * ezI18n({
19
- * locales: ['en', 'es', 'fr'],
20
- * defaultLocale: 'en',
21
- * translations: {
22
- * en: './src/i18n/en.json',
23
- * es: './src/i18n/es.json',
24
- * },
25
- * }),
26
- * ],
27
- * });
28
- */
29
- export default function ezI18n(config: EzI18nConfig): AstroIntegration {
30
- const resolved = resolveConfig(config);
31
-
32
- return {
33
- name: 'ez-i18n',
34
- hooks: {
35
- 'astro:config:setup': ({
36
- updateConfig,
37
- addMiddleware,
38
- injectScript,
39
- }: HookParameters<'astro:config:setup'>) => {
40
- // Add Vite plugin for virtual modules
41
- updateConfig({
42
- vite: {
43
- plugins: [vitePlugin(config)],
44
- },
45
- });
46
-
47
- // Add locale detection middleware
48
- addMiddleware({
49
- entrypoint: '@zachhandley/ez-i18n/middleware',
50
- order: 'pre',
51
- });
52
-
53
- // Inject hydration script to sync localStorage with cookie
54
- // This prevents hydration mismatch when server and client disagree
55
- const hydrationScript = `
56
- (function() {
57
- try {
58
- var cookieName = ${JSON.stringify(resolved.cookieName)};
59
- var stored = localStorage.getItem(cookieName);
60
- var cookieMatch = document.cookie.match(new RegExp(cookieName + '=([^;]+)'));
61
- var cookie = cookieMatch ? cookieMatch[1] : null;
62
- if (cookie && stored !== cookie) {
63
- localStorage.setItem(cookieName, cookie);
64
- }
65
- } catch (e) {}
66
- })();
67
- `;
68
- injectScript('head-inline', hydrationScript);
69
- },
70
-
71
- 'astro:config:done': ({
72
- injectTypes,
73
- }: HookParameters<'astro:config:done'>) => {
74
- // Inject type declarations for virtual modules
75
- injectTypes({
76
- filename: 'virtual.d.ts',
77
- content: `\
78
- declare module 'ez-i18n:config' {
79
- /** List of all supported locale codes */
80
- export const locales: readonly string[];
81
- /** Default locale when no preference is detected */
82
- export const defaultLocale: string;
83
- /** Cookie name used to store locale preference */
84
- export const cookieName: string;
85
- }
86
-
87
- declare module 'ez-i18n:runtime' {
88
- import type { ReadableAtom } from 'nanostores';
89
- /** Reactive store containing the current locale */
90
- export const locale: ReadableAtom<string>;
91
- /**
92
- * Translate a key to the current locale
93
- * @param key - Dot-notation key (e.g., 'common.welcome')
94
- * @param params - Optional interpolation params for {placeholder} syntax
95
- */
96
- export function t(key: string, params?: Record<string, string | number>): string;
97
- /**
98
- * Set the current locale and persist to cookie/localStorage
99
- * @param locale - Locale code to switch to
100
- * @param cookieName - Optional custom cookie name
101
- */
102
- export function setLocale(locale: string, cookieName?: string): Promise<void>;
103
- /**
104
- * Initialize the locale store with translations
105
- * @param locale - Initial locale code
106
- * @param translations - Optional initial translations object
107
- */
108
- export function initLocale(locale: string, translations?: Record<string, unknown>): void;
109
- }
110
-
111
- declare module 'ez-i18n:translations' {
112
- /** Load translations for a specific locale */
113
- export function loadTranslations(locale: string): Promise<Record<string, unknown>>;
114
- /** Get the translation loader map from config */
115
- export const translationLoaders: Record<string, () => Promise<{ default: Record<string, unknown> }>>;
116
- }
117
- `,
118
- });
119
- },
120
- },
121
- };
122
- }
1
+ import type { AstroIntegration, HookParameters } from 'astro';
2
+ import type { EzI18nConfig } from './types';
3
+ import { vitePlugin, resolveConfig } from './vite-plugin';
4
+
5
+ export type { EzI18nConfig, TranslateFunction } from './types';
6
+
7
+ /**
8
+ * ez-i18n Astro integration
9
+ *
10
+ * Provides cookie-based i18n without URL prefixes.
11
+ *
12
+ * @example
13
+ * // astro.config.ts
14
+ * import ezI18n from '@zachhandley/ez-i18n';
15
+ *
16
+ * export default defineConfig({
17
+ * integrations: [
18
+ * ezI18n({
19
+ * locales: ['en', 'es', 'fr'],
20
+ * defaultLocale: 'en',
21
+ * translations: {
22
+ * en: './src/i18n/en.json',
23
+ * es: './src/i18n/es.json',
24
+ * },
25
+ * }),
26
+ * ],
27
+ * });
28
+ */
29
+ export default function ezI18n(config: EzI18nConfig): AstroIntegration {
30
+ const resolved = resolveConfig(config);
31
+
32
+ return {
33
+ name: 'ez-i18n',
34
+ hooks: {
35
+ 'astro:config:setup': ({
36
+ updateConfig,
37
+ addMiddleware,
38
+ injectScript,
39
+ }: HookParameters<'astro:config:setup'>) => {
40
+ // Add Vite plugin for virtual modules
41
+ updateConfig({
42
+ vite: {
43
+ plugins: [vitePlugin(config)],
44
+ },
45
+ });
46
+
47
+ // Add locale detection middleware
48
+ addMiddleware({
49
+ entrypoint: '@zachhandley/ez-i18n/middleware',
50
+ order: 'pre',
51
+ });
52
+
53
+ // Inject hydration script to sync localStorage with cookie
54
+ // This prevents hydration mismatch when server and client disagree
55
+ const hydrationScript = `
56
+ (function() {
57
+ try {
58
+ var cookieName = ${JSON.stringify(resolved.cookieName)};
59
+ var stored = localStorage.getItem(cookieName);
60
+ var cookieMatch = document.cookie.match(new RegExp(cookieName + '=([^;]+)'));
61
+ var cookie = cookieMatch ? cookieMatch[1] : null;
62
+ if (cookie && stored !== cookie) {
63
+ localStorage.setItem(cookieName, cookie);
64
+ }
65
+ } catch (e) {}
66
+ })();
67
+ `;
68
+ injectScript('head-inline', hydrationScript);
69
+ },
70
+
71
+ 'astro:config:done': ({
72
+ injectTypes,
73
+ }: HookParameters<'astro:config:done'>) => {
74
+ // Inject type declarations for virtual modules
75
+ injectTypes({
76
+ filename: 'virtual.d.ts',
77
+ content: `\
78
+ declare module 'ez-i18n:config' {
79
+ /** List of all supported locale codes */
80
+ export const locales: readonly string[];
81
+ /** Default locale when no preference is detected */
82
+ export const defaultLocale: string;
83
+ /** Cookie name used to store locale preference */
84
+ export const cookieName: string;
85
+ }
86
+
87
+ declare module 'ez-i18n:runtime' {
88
+ import type { ReadableAtom } from 'nanostores';
89
+ /** Reactive store containing the current locale */
90
+ export const locale: ReadableAtom<string>;
91
+ /**
92
+ * Translate a key to the current locale
93
+ * @param key - Dot-notation key (e.g., 'common.welcome')
94
+ * @param params - Optional interpolation params for {placeholder} syntax
95
+ */
96
+ export function t(key: string, params?: Record<string, string | number>): string;
97
+ /**
98
+ * Set the current locale and persist to cookie/localStorage
99
+ * @param locale - Locale code to switch to
100
+ * @param cookieName - Optional custom cookie name
101
+ */
102
+ export function setLocale(locale: string, cookieName?: string): Promise<void>;
103
+ /**
104
+ * Initialize the locale store with translations
105
+ * @param locale - Initial locale code
106
+ * @param translations - Optional initial translations object
107
+ */
108
+ export function initLocale(locale: string, translations?: Record<string, unknown>): void;
109
+ }
110
+
111
+ declare module 'ez-i18n:translations' {
112
+ /** Load translations for a specific locale */
113
+ export function loadTranslations(locale: string): Promise<Record<string, unknown>>;
114
+ /** Get the translation loader map from config */
115
+ export const translationLoaders: Record<string, () => Promise<{ default: Record<string, unknown> }>>;
116
+ }
117
+ `,
118
+ });
119
+ },
120
+ },
121
+ };
122
+ }
package/src/middleware.ts CHANGED
@@ -1,61 +1,61 @@
1
- import { defineMiddleware } from 'astro:middleware';
2
-
3
- /**
4
- * Locale detection middleware for ez-i18n
5
- *
6
- * Detection priority:
7
- * 1. ?lang query parameter (allows explicit switching)
8
- * 2. Cookie value
9
- * 3. Accept-Language header
10
- * 4. Default locale
11
- */
12
- export const onRequest = defineMiddleware(async ({ cookies, request, locals }, next) => {
13
- // Import config from virtual module (provided by vite-plugin)
14
- const { locales, defaultLocale, cookieName } = await import('ez-i18n:config');
15
-
16
- const url = new URL(request.url);
17
-
18
- // Priority 1: Query parameter
19
- const langParam = url.searchParams.get('lang');
20
-
21
- // Priority 2: Cookie
22
- const cookieValue = cookies.get(cookieName)?.value;
23
-
24
- // Priority 3: Accept-Language header
25
- const acceptLang = request.headers.get('accept-language');
26
- const browserLang = acceptLang?.split(',')[0]?.split('-')[0];
27
-
28
- // Determine locale with priority
29
- let locale = defaultLocale;
30
-
31
- if (langParam && locales.includes(langParam)) {
32
- locale = langParam;
33
- } else if (cookieValue && locales.includes(cookieValue)) {
34
- locale = cookieValue;
35
- } else if (browserLang && locales.includes(browserLang)) {
36
- locale = browserLang;
37
- }
38
-
39
- // Set locale on locals for use in pages
40
- locals.locale = locale;
41
-
42
- // Load translations for the current locale
43
- try {
44
- const { loadTranslations } = await import('ez-i18n:translations');
45
- locals.translations = await loadTranslations(locale);
46
- } catch {
47
- // Fallback to empty translations if loader not configured
48
- locals.translations = {};
49
- }
50
-
51
- // Update cookie if changed via query param
52
- if (langParam && langParam !== cookieValue && locales.includes(langParam)) {
53
- cookies.set(cookieName, locale, {
54
- path: '/',
55
- maxAge: 60 * 60 * 24 * 365, // 1 year
56
- sameSite: 'lax',
57
- });
58
- }
59
-
60
- return next();
61
- });
1
+ import { defineMiddleware } from 'astro:middleware';
2
+
3
+ /**
4
+ * Locale detection middleware for ez-i18n
5
+ *
6
+ * Detection priority:
7
+ * 1. ?lang query parameter (allows explicit switching)
8
+ * 2. Cookie value
9
+ * 3. Accept-Language header
10
+ * 4. Default locale
11
+ */
12
+ export const onRequest = defineMiddleware(async ({ cookies, request, locals }, next) => {
13
+ // Import config from virtual module (provided by vite-plugin)
14
+ const { locales, defaultLocale, cookieName } = await import('ez-i18n:config');
15
+
16
+ const url = new URL(request.url);
17
+
18
+ // Priority 1: Query parameter
19
+ const langParam = url.searchParams.get('lang');
20
+
21
+ // Priority 2: Cookie
22
+ const cookieValue = cookies.get(cookieName)?.value;
23
+
24
+ // Priority 3: Accept-Language header
25
+ const acceptLang = request.headers.get('accept-language');
26
+ const browserLang = acceptLang?.split(',')[0]?.split('-')[0];
27
+
28
+ // Determine locale with priority
29
+ let locale = defaultLocale;
30
+
31
+ if (langParam && locales.includes(langParam)) {
32
+ locale = langParam;
33
+ } else if (cookieValue && locales.includes(cookieValue)) {
34
+ locale = cookieValue;
35
+ } else if (browserLang && locales.includes(browserLang)) {
36
+ locale = browserLang;
37
+ }
38
+
39
+ // Set locale on locals for use in pages
40
+ locals.locale = locale;
41
+
42
+ // Load translations for the current locale
43
+ try {
44
+ const { loadTranslations } = await import('ez-i18n:translations');
45
+ locals.translations = await loadTranslations(locale);
46
+ } catch {
47
+ // Fallback to empty translations if loader not configured
48
+ locals.translations = {};
49
+ }
50
+
51
+ // Update cookie if changed via query param
52
+ if (langParam && langParam !== cookieValue && locales.includes(langParam)) {
53
+ cookies.set(cookieName, locale, {
54
+ path: '/',
55
+ maxAge: 60 * 60 * 24 * 365, // 1 year
56
+ sameSite: 'lax',
57
+ });
58
+ }
59
+
60
+ return next();
61
+ });
@@ -1,19 +1,19 @@
1
- /**
2
- * Runtime exports for ez-i18n
3
- *
4
- * This module is imported by the ez-i18n:runtime virtual module
5
- * and can also be used directly in Vue components
6
- */
7
- export {
8
- effectiveLocale,
9
- translations,
10
- localePreference,
11
- localeLoading,
12
- initLocale,
13
- setLocale,
14
- setTranslations,
15
- getLocale,
16
- getTranslations,
17
- } from './store';
18
-
19
- export type { TranslationLoader } from './store';
1
+ /**
2
+ * Runtime exports for ez-i18n
3
+ *
4
+ * This module is imported by the ez-i18n:runtime virtual module
5
+ * and can also be used directly in Vue components
6
+ */
7
+ export {
8
+ effectiveLocale,
9
+ translations,
10
+ localePreference,
11
+ localeLoading,
12
+ initLocale,
13
+ setLocale,
14
+ setTranslations,
15
+ getLocale,
16
+ getTranslations,
17
+ } from './store';
18
+
19
+ export type { TranslationLoader } from './store';