@zachhandley/ez-i18n 0.1.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/README.md +211 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +212 -0
- package/dist/middleware.js +37 -0
- package/dist/runtime/index.d.ts +51 -0
- package/dist/runtime/index.js +68 -0
- package/dist/runtime/vue-plugin.d.ts +52 -0
- package/dist/runtime/vue-plugin.js +110 -0
- package/dist/types-DwCG8sp8.d.ts +48 -0
- package/package.json +89 -0
- package/src/components/EzI18nHead.astro +62 -0
- package/src/index.ts +122 -0
- package/src/middleware.ts +61 -0
- package/src/runtime/index.ts +19 -0
- package/src/runtime/store.ts +122 -0
- package/src/runtime/vue-plugin.ts +137 -0
- package/src/types.ts +61 -0
- package/src/virtual.d.ts +64 -0
- package/src/vite-plugin.ts +146 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { App, Plugin, ComputedRef } from 'vue';
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useStore } from '@nanostores/vue';
|
|
4
|
+
import { effectiveLocale, translations, setLocale } from './store';
|
|
5
|
+
import type { TranslateFunction } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get nested value from object using dot notation
|
|
9
|
+
*/
|
|
10
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
11
|
+
const keys = path.split('.');
|
|
12
|
+
let value: unknown = obj;
|
|
13
|
+
|
|
14
|
+
for (const key of keys) {
|
|
15
|
+
if (value == null || typeof value !== 'object') {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
value = (value as Record<string, unknown>)[key];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Interpolate params into string
|
|
26
|
+
*/
|
|
27
|
+
function interpolate(
|
|
28
|
+
str: string,
|
|
29
|
+
params?: Record<string, string | number>
|
|
30
|
+
): string {
|
|
31
|
+
if (!params) return str;
|
|
32
|
+
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
|
33
|
+
return key in params ? String(params[key]) : match;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a translation function bound to a translations object
|
|
39
|
+
*/
|
|
40
|
+
function createTranslateFunction(
|
|
41
|
+
translationsRef: ComputedRef<Record<string, unknown>>
|
|
42
|
+
): TranslateFunction {
|
|
43
|
+
return (key: string, params?: Record<string, string | number>): string => {
|
|
44
|
+
const trans = translationsRef.value;
|
|
45
|
+
const value = getNestedValue(trans, key);
|
|
46
|
+
|
|
47
|
+
if (typeof value !== 'string') {
|
|
48
|
+
if (import.meta.env?.DEV) {
|
|
49
|
+
console.warn('[ez-i18n] Missing translation:', key);
|
|
50
|
+
}
|
|
51
|
+
return key;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return interpolate(value, params);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Vue plugin that provides global $t(), $locale, and $setLocale
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // In _vueEntrypoint.ts or main.ts
|
|
63
|
+
* import { ezI18nVue } from 'ez-i18n/vue';
|
|
64
|
+
*
|
|
65
|
+
* export default (app) => {
|
|
66
|
+
* app.use(ezI18nVue);
|
|
67
|
+
* };
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // In Vue components
|
|
71
|
+
* <template>
|
|
72
|
+
* <h1>{{ $t('welcome.title') }}</h1>
|
|
73
|
+
* <p>{{ $t('welcome.message', { name: userName }) }}</p>
|
|
74
|
+
* <button @click="$setLocale('es')">Español</button>
|
|
75
|
+
* </template>
|
|
76
|
+
*/
|
|
77
|
+
export const ezI18nVue: Plugin = {
|
|
78
|
+
install(app: App) {
|
|
79
|
+
// Get reactive store values
|
|
80
|
+
const locale = useStore(effectiveLocale);
|
|
81
|
+
const trans = useStore(translations);
|
|
82
|
+
|
|
83
|
+
// Create reactive computed for translations
|
|
84
|
+
const transComputed = computed(() => trans.value);
|
|
85
|
+
|
|
86
|
+
// Create translate function
|
|
87
|
+
const t = createTranslateFunction(transComputed);
|
|
88
|
+
|
|
89
|
+
// Add global properties
|
|
90
|
+
app.config.globalProperties.$t = t;
|
|
91
|
+
app.config.globalProperties.$locale = locale;
|
|
92
|
+
app.config.globalProperties.$setLocale = setLocale;
|
|
93
|
+
|
|
94
|
+
// Also provide for composition API usage
|
|
95
|
+
app.provide('ez-i18n', {
|
|
96
|
+
t,
|
|
97
|
+
locale,
|
|
98
|
+
setLocale,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Composable for using i18n in Vue components with Composition API
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* <script setup>
|
|
108
|
+
* import { useI18n } from 'ez-i18n/vue';
|
|
109
|
+
*
|
|
110
|
+
* const { t, locale, setLocale } = useI18n();
|
|
111
|
+
* const greeting = t('welcome.greeting');
|
|
112
|
+
* </script>
|
|
113
|
+
*/
|
|
114
|
+
export function useI18n() {
|
|
115
|
+
const locale = useStore(effectiveLocale);
|
|
116
|
+
const trans = useStore(translations);
|
|
117
|
+
const transComputed = computed(() => trans.value);
|
|
118
|
+
const t = createTranslateFunction(transComputed);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
t,
|
|
122
|
+
locale,
|
|
123
|
+
setLocale,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Type augmentation for Vue global properties
|
|
128
|
+
declare module 'vue' {
|
|
129
|
+
interface ComponentCustomProperties {
|
|
130
|
+
$t: TranslateFunction;
|
|
131
|
+
/** Current locale (reactive ref from nanostore) */
|
|
132
|
+
$locale: Readonly<import('vue').Ref<string>>;
|
|
133
|
+
$setLocale: typeof setLocale;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default ezI18nVue;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for ez-i18n Astro integration
|
|
3
|
+
*/
|
|
4
|
+
export interface EzI18nConfig {
|
|
5
|
+
/**
|
|
6
|
+
* List of supported locale codes (e.g., ['en', 'es', 'fr'])
|
|
7
|
+
*/
|
|
8
|
+
locales: string[];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default locale to use when no preference is detected
|
|
12
|
+
*/
|
|
13
|
+
defaultLocale: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cookie name for storing locale preference
|
|
17
|
+
* @default 'ez-locale'
|
|
18
|
+
*/
|
|
19
|
+
cookieName?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Translation file paths for each locale.
|
|
23
|
+
* Paths are relative to your project root.
|
|
24
|
+
* Dynamic imports will be generated for code splitting.
|
|
25
|
+
* @example
|
|
26
|
+
* translations: {
|
|
27
|
+
* en: './src/i18n/en.json',
|
|
28
|
+
* es: './src/i18n/es.json',
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
translations?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolved config with defaults applied
|
|
36
|
+
*/
|
|
37
|
+
export interface ResolvedEzI18nConfig {
|
|
38
|
+
locales: string[];
|
|
39
|
+
defaultLocale: string;
|
|
40
|
+
cookieName: string;
|
|
41
|
+
translations: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Translation function type
|
|
46
|
+
*/
|
|
47
|
+
export type TranslateFunction = (key: string, params?: Record<string, string | number>) => string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Augment Astro's locals type
|
|
51
|
+
*/
|
|
52
|
+
declare global {
|
|
53
|
+
namespace App {
|
|
54
|
+
interface Locals {
|
|
55
|
+
/** Current locale code */
|
|
56
|
+
locale: string;
|
|
57
|
+
/** Loaded translations for the current locale */
|
|
58
|
+
translations: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/virtual.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for ez-i18n virtual modules
|
|
3
|
+
*
|
|
4
|
+
* Note: These are also injected via injectTypes() for consumer projects.
|
|
5
|
+
* This file provides types during package development.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare module 'ez-i18n:config' {
|
|
9
|
+
/** List of all supported locale codes */
|
|
10
|
+
export const locales: readonly string[];
|
|
11
|
+
/** Default locale when no preference is detected */
|
|
12
|
+
export const defaultLocale: string;
|
|
13
|
+
/** Cookie name used to store locale preference */
|
|
14
|
+
export const cookieName: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare module 'ez-i18n:runtime' {
|
|
18
|
+
import type { ReadableAtom } from 'nanostores';
|
|
19
|
+
|
|
20
|
+
/** Reactive store containing the current locale */
|
|
21
|
+
export const locale: ReadableAtom<string>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Translate a key to the current locale
|
|
25
|
+
* @param key - Dot-notation key (e.g., 'common.welcome')
|
|
26
|
+
* @param params - Optional interpolation params for {placeholder} syntax
|
|
27
|
+
*/
|
|
28
|
+
export function t(key: string, params?: Record<string, string | number>): string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set the current locale and persist to cookie/localStorage
|
|
32
|
+
* @param locale - Locale code to switch to
|
|
33
|
+
* @param options - Cookie name string or options object
|
|
34
|
+
*/
|
|
35
|
+
export function setLocale(
|
|
36
|
+
locale: string,
|
|
37
|
+
options?: string | {
|
|
38
|
+
cookieName?: string;
|
|
39
|
+
loadTranslations?: () => Promise<{ default?: Record<string, unknown> } | Record<string, unknown>>;
|
|
40
|
+
}
|
|
41
|
+
): Promise<void>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Initialize the locale store with translations
|
|
45
|
+
* @param locale - Initial locale code
|
|
46
|
+
* @param translations - Optional initial translations object
|
|
47
|
+
*/
|
|
48
|
+
export function initLocale(locale: string, translations?: Record<string, unknown>): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare module 'ez-i18n:translations' {
|
|
52
|
+
/** Map of locale codes to their translation loaders */
|
|
53
|
+
export const translationLoaders: Record<
|
|
54
|
+
string,
|
|
55
|
+
() => Promise<{ default?: Record<string, unknown> } | Record<string, unknown>>
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load translations for a specific locale
|
|
60
|
+
* @param locale - Locale code to load translations for
|
|
61
|
+
* @returns Translations object or empty object if not found
|
|
62
|
+
*/
|
|
63
|
+
export function loadTranslations(locale: string): Promise<Record<string, unknown>>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import type { EzI18nConfig, ResolvedEzI18nConfig } from './types';
|
|
3
|
+
|
|
4
|
+
const VIRTUAL_CONFIG = 'ez-i18n:config';
|
|
5
|
+
const VIRTUAL_RUNTIME = 'ez-i18n:runtime';
|
|
6
|
+
const VIRTUAL_TRANSLATIONS = 'ez-i18n:translations';
|
|
7
|
+
const RESOLVED_PREFIX = '\0';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve config with defaults
|
|
11
|
+
*/
|
|
12
|
+
export function resolveConfig(config: EzI18nConfig): ResolvedEzI18nConfig {
|
|
13
|
+
return {
|
|
14
|
+
locales: config.locales,
|
|
15
|
+
defaultLocale: config.defaultLocale,
|
|
16
|
+
cookieName: config.cookieName ?? 'ez-locale',
|
|
17
|
+
translations: config.translations ?? {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Vite plugin that provides virtual modules for ez-i18n
|
|
23
|
+
*/
|
|
24
|
+
export function vitePlugin(config: EzI18nConfig): Plugin {
|
|
25
|
+
const resolved = resolveConfig(config);
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
name: 'ez-i18n-vite',
|
|
29
|
+
enforce: 'pre',
|
|
30
|
+
|
|
31
|
+
resolveId(id) {
|
|
32
|
+
if (id === VIRTUAL_CONFIG || id === VIRTUAL_RUNTIME || id === VIRTUAL_TRANSLATIONS) {
|
|
33
|
+
return RESOLVED_PREFIX + id;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
load(id) {
|
|
39
|
+
// ez-i18n:config - Static config values
|
|
40
|
+
if (id === RESOLVED_PREFIX + VIRTUAL_CONFIG) {
|
|
41
|
+
return `
|
|
42
|
+
export const locales = ${JSON.stringify(resolved.locales)};
|
|
43
|
+
export const defaultLocale = ${JSON.stringify(resolved.defaultLocale)};
|
|
44
|
+
export const cookieName = ${JSON.stringify(resolved.cookieName)};
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ez-i18n:runtime - Runtime exports for Astro files
|
|
49
|
+
if (id === RESOLVED_PREFIX + VIRTUAL_RUNTIME) {
|
|
50
|
+
return `
|
|
51
|
+
import { effectiveLocale, translations, setLocale, initLocale } from '@zachhandley/ez-i18n/runtime';
|
|
52
|
+
|
|
53
|
+
export { setLocale, initLocale };
|
|
54
|
+
export { effectiveLocale as locale };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get nested value from object using dot notation
|
|
58
|
+
*/
|
|
59
|
+
function getNestedValue(obj, path) {
|
|
60
|
+
const keys = path.split('.');
|
|
61
|
+
let value = obj;
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
if (value == null || typeof value !== 'object') return undefined;
|
|
64
|
+
value = value[key];
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Interpolate params into string
|
|
71
|
+
*/
|
|
72
|
+
function interpolate(str, params) {
|
|
73
|
+
if (!params) return str;
|
|
74
|
+
return str.replace(/\\{(\\w+)\\}/g, (match, key) => {
|
|
75
|
+
return key in params ? String(params[key]) : match;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Translate a key to the current locale
|
|
81
|
+
* @param key - Dot-notation key (e.g., 'common.welcome')
|
|
82
|
+
* @param params - Optional interpolation params
|
|
83
|
+
*/
|
|
84
|
+
export function t(key, params) {
|
|
85
|
+
const trans = translations.get();
|
|
86
|
+
const value = getNestedValue(trans, key);
|
|
87
|
+
|
|
88
|
+
if (typeof value !== 'string') {
|
|
89
|
+
if (import.meta.env.DEV) {
|
|
90
|
+
console.warn('[ez-i18n] Missing translation:', key);
|
|
91
|
+
}
|
|
92
|
+
return key;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return interpolate(value, params);
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ez-i18n:translations - Translation loaders
|
|
101
|
+
if (id === RESOLVED_PREFIX + VIRTUAL_TRANSLATIONS) {
|
|
102
|
+
// Generate dynamic import statements for each locale
|
|
103
|
+
const loaderEntries = Object.entries(resolved.translations)
|
|
104
|
+
.map(([locale, path]) => ` ${JSON.stringify(locale)}: () => import(${JSON.stringify(path)})`)
|
|
105
|
+
.join(',\n');
|
|
106
|
+
|
|
107
|
+
return `
|
|
108
|
+
/**
|
|
109
|
+
* Translation loaders for ez-i18n
|
|
110
|
+
* Auto-generated from config
|
|
111
|
+
*/
|
|
112
|
+
export const translationLoaders = {
|
|
113
|
+
${loaderEntries}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Load translations for a specific locale
|
|
118
|
+
* @param locale - Locale code to load translations for
|
|
119
|
+
* @returns Translations object or empty object if not found
|
|
120
|
+
*/
|
|
121
|
+
export async function loadTranslations(locale) {
|
|
122
|
+
const loader = translationLoaders[locale];
|
|
123
|
+
if (!loader) {
|
|
124
|
+
if (import.meta.env.DEV) {
|
|
125
|
+
console.warn('[ez-i18n] No translations configured for locale:', locale);
|
|
126
|
+
}
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const mod = await loader();
|
|
132
|
+
return mod.default ?? mod;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (import.meta.env.DEV) {
|
|
135
|
+
console.error('[ez-i18n] Failed to load translations for locale:', locale, error);
|
|
136
|
+
}
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|