@i18n-micro/astro 1.3.4 → 1.3.6

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.
@@ -1,4 +1,5 @@
1
1
  import { Translations } from '@i18n-micro/types';
2
+ import { TranslationFileBuckets } from '@i18n-micro/utils/parse-path';
2
3
  /**
3
4
  * WARNING: Node.js-only functions
4
5
  *
@@ -20,9 +21,7 @@ export interface LoadTranslationsOptions {
20
21
  rootDir?: string;
21
22
  disablePageLocales?: boolean;
22
23
  }
23
- export interface LoadedTranslations {
24
- root: Record<string, Translations>;
25
- routes: Record<string, Record<string, Translations>>;
24
+ export interface LoadedTranslations extends TranslationFileBuckets<Translations> {
26
25
  }
27
26
  /**
28
27
  * Load all translations from a directory
package/package.json CHANGED
@@ -1,22 +1,31 @@
1
1
  {
2
2
  "name": "@i18n-micro/astro",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Astro integration for i18n-micro — translations, routing, and islands.",
5
- "type": "module",
5
+ "keywords": [
6
+ "astro",
7
+ "i18n",
8
+ "internationalization",
9
+ "translations"
10
+ ],
11
+ "homepage": "https://github.com/s00d/nuxt-i18n-micro/tree/main/packages/astro#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/s00d/nuxt-i18n-micro/issues"
14
+ },
6
15
  "license": "MIT",
7
- "sideEffects": false,
8
16
  "repository": {
9
17
  "type": "git",
10
18
  "url": "git+https://github.com/s00d/nuxt-i18n-micro.git",
11
19
  "directory": "packages/astro"
12
20
  },
13
- "homepage": "https://github.com/s00d/nuxt-i18n-micro/tree/main/packages/astro#readme",
14
- "bugs": {
15
- "url": "https://github.com/s00d/nuxt-i18n-micro/issues"
16
- },
17
- "engines": {
18
- "node": ">=18"
19
- },
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
20
29
  "main": "./dist/index.cjs",
21
30
  "types": "./dist/index.d.ts",
22
31
  "exports": {
@@ -64,32 +73,35 @@
64
73
  },
65
74
  "./package.json": "./package.json"
66
75
  },
67
- "files": [
68
- "dist",
69
- "src",
70
- "README.md",
71
- "LICENSE"
72
- ],
73
76
  "publishConfig": {
74
77
  "access": "public"
75
78
  },
76
- "keywords": [
77
- "astro",
78
- "i18n",
79
- "internationalization",
80
- "translations"
81
- ],
82
79
  "dependencies": {
83
- "@i18n-micro/core": "1.3.1",
84
- "@i18n-micro/node": "1.2.1",
85
- "@i18n-micro/types": "1.2.2"
80
+ "@i18n-micro/utils": "1.0.1",
81
+ "@i18n-micro/core": "1.3.2",
82
+ "@i18n-micro/node": "1.2.3",
83
+ "@i18n-micro/types": "1.2.4"
86
84
  },
87
- "peerDependencies": {
85
+ "devDependencies": {
86
+ "@types/jest": "^29.5.14",
88
87
  "astro": "^5.16.5",
88
+ "jest": "^29.7.0",
89
+ "preact": "^10.0.0",
90
+ "publint": "^0.3.17",
91
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
89
92
  "svelte": "^4.0.0 || ^5.0.0",
90
- "vue": "^3.5.25",
93
+ "ts-jest": "^29.4.6",
94
+ "vite": "^7.3.1",
95
+ "vite-plugin-dts": "^4.5.4",
96
+ "vitest": "^3.2.4",
97
+ "vue": "^3.5.25"
98
+ },
99
+ "peerDependencies": {
100
+ "astro": "^5.16.5",
101
+ "preact": "^10.0.0",
91
102
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
92
- "preact": "^10.0.0"
103
+ "svelte": "^4.0.0 || ^5.0.0",
104
+ "vue": "^3.5.25"
93
105
  },
94
106
  "peerDependenciesMeta": {
95
107
  "svelte": {
@@ -105,19 +117,8 @@
105
117
  "optional": true
106
118
  }
107
119
  },
108
- "devDependencies": {
109
- "astro": "^5.16.5",
110
- "vite": "^7.3.1",
111
- "vite-plugin-dts": "^4.5.4",
112
- "jest": "^29.7.0",
113
- "ts-jest": "^29.4.6",
114
- "@types/jest": "^29.5.14",
115
- "svelte": "^4.0.0 || ^5.0.0",
116
- "vue": "^3.5.25",
117
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
118
- "preact": "^10.0.0",
119
- "publint": "^0.3.17",
120
- "vitest": "^3.2.4"
120
+ "engines": {
121
+ "node": ">=18"
121
122
  },
122
123
  "scripts": {
123
124
  "build": "vite build",
package/src/index.ts CHANGED
@@ -4,15 +4,7 @@ import './env.d'
4
4
  // Re-export utilities from core
5
5
  export { defaultPlural, FormatService, interpolate } from '@i18n-micro/core'
6
6
  // Re-export types from @i18n-micro/types
7
- export type {
8
- CleanTranslation,
9
- Getter,
10
- Locale,
11
- LocaleCode,
12
- Params,
13
- PluralFunc,
14
- Translations,
15
- } from '@i18n-micro/types'
7
+ export type { CleanTranslation, Getter, Locale, LocaleCode, Params, PluralFunc, Translations } from '@i18n-micro/types'
16
8
  export { AstroI18n, type AstroI18nOptions } from './composer'
17
9
  // Export integration options type
18
10
  export type { I18nIntegrationOptions } from './integration'
@@ -20,33 +12,13 @@ export type { I18nIntegrationOptions } from './integration'
20
12
  export { createI18n, i18nIntegration } from './integration'
21
13
  export type { LoadedTranslations, LoadTranslationsOptions } from './load-translations'
22
14
  // Export translation loading utilities
23
- export {
24
- loadTranslationsFromDir,
25
- loadTranslationsIntoI18n,
26
- } from './load-translations'
15
+ export { loadTranslationsFromDir, loadTranslationsIntoI18n } from './load-translations'
27
16
  export type { I18nMiddlewareOptions } from './middleware'
28
17
  export { createI18nMiddleware, detectLocale } from './middleware'
29
18
  export { createAstroRouterAdapter } from './router/adapter'
30
19
  // Router abstraction
31
20
  export type { I18nRoutingStrategy } from './router/types'
32
- // Legacy routing utilities (deprecated, use routingStrategy instead)
33
- // Kept for backward compatibility
34
- export {
35
- getLocaleFromPath,
36
- getRouteName,
37
- localizePath,
38
- removeLocaleFromPath,
39
- switchLocalePath,
40
- } from './routing'
41
21
  export type { I18nClientProps, LocaleHeadOptions, LocaleHeadResult } from './utils'
42
22
  // Utilities
43
- export {
44
- getDefaultLocale,
45
- getI18n,
46
- getI18nProps,
47
- getLocale,
48
- getLocales,
49
- useI18n,
50
- useLocaleHead,
51
- } from './utils'
23
+ export { getDefaultLocale, getI18n, getI18nProps, getLocale, getLocales, useI18n, useLocaleHead } from './utils'
52
24
  // It's loaded directly by Astro via entrypoint in integration config
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
2
- import { join, resolve } from 'node:path'
2
+ import { join, relative, resolve, sep } from 'node:path'
3
3
  import type { Translations } from '@i18n-micro/types'
4
+ import { mergeRouteTranslationsWithRoot, storeLoadedTranslationFile, type TranslationFileBuckets } from '@i18n-micro/utils/parse-path'
4
5
 
5
6
  /**
6
7
  * WARNING: Node.js-only functions
@@ -25,9 +26,24 @@ export interface LoadTranslationsOptions {
25
26
  disablePageLocales?: boolean
26
27
  }
27
28
 
28
- export interface LoadedTranslations {
29
- root: Record<string, Translations>
30
- routes: Record<string, Record<string, Translations>>
29
+ export interface LoadedTranslations extends TranslationFileBuckets<Translations> {}
30
+
31
+ function walkTranslationFiles(dir: string, onFile: (fullPath: string) => void): void {
32
+ if (!existsSync(dir)) return
33
+
34
+ for (const entry of readdirSync(dir)) {
35
+ const fullPath = join(dir, entry)
36
+ const stat = statSync(fullPath)
37
+
38
+ if (stat.isDirectory()) {
39
+ walkTranslationFiles(fullPath, onFile)
40
+ continue
41
+ }
42
+
43
+ if (entry.endsWith('.json')) {
44
+ onFile(fullPath)
45
+ }
46
+ }
31
47
  }
32
48
 
33
49
  /**
@@ -46,56 +62,21 @@ export function loadTranslationsFromDir(options: LoadTranslationsOptions): Loade
46
62
  return { root: {}, routes: {} }
47
63
  }
48
64
 
49
- const root: Record<string, Translations> = {}
50
- const routes: Record<string, Record<string, Translations>> = {}
51
-
52
- /**
53
- * Recursively load translation files
54
- */
55
- const loadFiles = (dir: string, routePrefix = ''): void => {
56
- if (!existsSync(dir)) return
57
-
58
- const entries = readdirSync(dir)
59
- for (const entry of entries) {
60
- const fullPath = join(dir, entry)
61
- const stat = statSync(fullPath)
62
-
63
- if (stat.isDirectory()) {
64
- // If it's a 'pages' directory and page locales are enabled, treat as route-specific
65
- if (entry === 'pages' && !disablePageLocales) {
66
- loadFiles(fullPath, '')
67
- } else if (routePrefix || disablePageLocales) {
68
- loadFiles(fullPath, routePrefix)
69
- } else {
70
- // This is a route directory (e.g., pages/home/)
71
- loadFiles(fullPath, entry)
72
- }
73
- } else if (entry.endsWith('.json')) {
74
- const locale = entry.replace('.json', '')
75
- try {
76
- const content = readFileSync(fullPath, 'utf-8')
77
- const translations = JSON.parse(content) as Translations
78
-
79
- if (routePrefix && !disablePageLocales) {
80
- // Route-specific translation
81
- if (!routes[routePrefix]) {
82
- routes[routePrefix] = {}
83
- }
84
- routes[routePrefix][locale] = translations
85
- } else {
86
- // Root-level translation
87
- root[locale] = translations
88
- }
89
- } catch (error) {
90
- console.error(`[i18n] Failed to load translation file: ${fullPath}`, error)
91
- }
92
- }
93
- }
94
- }
65
+ const buckets: LoadedTranslations = { root: {}, routes: {} }
95
66
 
96
- loadFiles(fullTranslationDir)
67
+ walkTranslationFiles(fullTranslationDir, (fullPath) => {
68
+ const relativePath = relative(fullTranslationDir, fullPath).split(sep).join('/')
69
+
70
+ try {
71
+ const content = readFileSync(fullPath, 'utf-8')
72
+ const translations = JSON.parse(content) as Translations
73
+ storeLoadedTranslationFile(buckets, relativePath, translations, disablePageLocales)
74
+ } catch (error) {
75
+ console.error(`[i18n] Failed to load translation file: ${fullPath}`, error)
76
+ }
77
+ })
97
78
 
98
- return { root, routes }
79
+ return buckets
99
80
  }
100
81
 
101
82
  /**
@@ -113,16 +94,13 @@ export function loadTranslationsIntoI18n(
113
94
  ): void {
114
95
  const { root, routes } = loadTranslationsFromDir(options)
115
96
 
116
- // Load root-level translations as index
117
97
  for (const [locale, translations] of Object.entries(root)) {
118
98
  i18n.addTranslations(locale, translations, false)
119
99
  }
120
100
 
121
- // Load route-specific translations (with root baked in)
122
101
  for (const [routeName, routeTranslations] of Object.entries(routes)) {
123
102
  for (const [locale, translations] of Object.entries(routeTranslations)) {
124
- const base = root[locale] || {}
125
- i18n.addRouteTranslations(locale, routeName, { ...base, ...translations }, false)
103
+ i18n.addRouteTranslations(locale, routeName, mergeRouteTranslationsWithRoot(root[locale], translations), false)
126
104
  }
127
105
  }
128
106
  }
package/src/middleware.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Locale } from '@i18n-micro/types'
2
+ import { detectLocaleFromAcceptLanguage } from '@i18n-micro/utils/accept-language'
2
3
  import type { MiddlewareHandler } from 'astro'
3
4
  import type { AstroI18n } from './composer'
4
5
  import { getGlobalRoutingStrategy } from './integration'
@@ -109,32 +110,6 @@ export function createI18nMiddleware(options: I18nMiddlewareOptions): Middleware
109
110
  }
110
111
  }
111
112
 
112
- /**
113
- * Parse Accept-Language header
114
- */
115
- function parseAcceptLanguage(acceptLanguage: string): string[] {
116
- const languages: string[] = []
117
- const parts = acceptLanguage.split(',')
118
-
119
- for (const part of parts) {
120
- const [lang, q = '1.0'] = part.trim().split(';q=')
121
- const quality = Number.parseFloat(q)
122
- if (quality > 0 && lang) {
123
- // Extract base language (e.g., 'en-US' -> 'en')
124
- const baseLang = lang.split('-')[0]?.toLowerCase()
125
- if (baseLang) {
126
- languages.push(baseLang)
127
- // Also add full locale if different
128
- if (lang !== baseLang) {
129
- languages.push(lang.toLowerCase())
130
- }
131
- }
132
- }
133
- }
134
-
135
- return languages
136
- }
137
-
138
113
  /**
139
114
  * Detect locale from request
140
115
  */
@@ -173,15 +148,9 @@ export function detectLocale(
173
148
  // 3. Try Accept-Language
174
149
  if (locale === defaultLocale) {
175
150
  try {
176
- const acceptLanguage = headers.get('accept-language')
177
- if (acceptLanguage) {
178
- const preferredLocales = parseAcceptLanguage(acceptLanguage)
179
- for (const preferred of preferredLocales) {
180
- if (locales.includes(preferred)) {
181
- locale = preferred
182
- break
183
- }
184
- }
151
+ const detectedFromHeader = detectLocaleFromAcceptLanguage(headers.get('accept-language') ?? undefined, locales)
152
+ if (detectedFromHeader) {
153
+ locale = detectedFromHeader
185
154
  }
186
155
  } catch {
187
156
  // Headers may be unavailable in static mode
package/dist/routing.d.ts DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * Get route name from Astro path
3
- * Extracts route name from path (e.g., /en/about -> about)
4
- */
5
- export declare function getRouteName(path: string, locales?: string[]): string;
6
- /**
7
- * Get locale from path
8
- * Checks if first segment is a locale code
9
- */
10
- export declare function getLocaleFromPath(path: string, defaultLocale?: string, locales?: string[]): string;
11
- /**
12
- * Switch locale in path
13
- * Replaces or adds locale prefix to path
14
- */
15
- export declare function switchLocalePath(path: string, newLocale: string, locales?: string[], defaultLocale?: string): string;
16
- /**
17
- * Localize path with locale prefix
18
- */
19
- export declare function localizePath(path: string, locale: string, locales?: string[], defaultLocale?: string): string;
20
- /**
21
- * Remove locale from path
22
- */
23
- export declare function removeLocaleFromPath(path: string, locales?: string[]): string;
package/src/routing.ts DELETED
@@ -1,94 +0,0 @@
1
- /**
2
- * Get route name from Astro path
3
- * Extracts route name from path (e.g., /en/about -> about)
4
- */
5
- export function getRouteName(path: string, locales: string[] = []): string {
6
- // Normalize the path
7
- const cleanPath = path.replace(/^\//, '').replace(/\/$/, '')
8
-
9
- if (!cleanPath) {
10
- return 'index'
11
- }
12
-
13
- const segments = cleanPath.split('/').filter(Boolean)
14
-
15
- // Remove locale from path if present
16
- const firstSegment = segments[0]
17
- if (firstSegment && locales.includes(firstSegment)) {
18
- segments.shift()
19
- }
20
-
21
- if (segments.length === 0) {
22
- return 'index'
23
- }
24
-
25
- return segments.join('-')
26
- }
27
-
28
- /**
29
- * Get locale from path
30
- * Checks if first segment is a locale code
31
- */
32
- export function getLocaleFromPath(path: string, defaultLocale: string = 'en', locales: string[] = []): string {
33
- const segments = path.split('/').filter(Boolean)
34
- const firstSegment = segments[0]
35
- if (firstSegment && locales.includes(firstSegment)) {
36
- return firstSegment
37
- }
38
-
39
- return defaultLocale
40
- }
41
-
42
- /**
43
- * Switch locale in path
44
- * Replaces or adds locale prefix to path
45
- */
46
- export function switchLocalePath(path: string, newLocale: string, locales: string[] = [], defaultLocale?: string): string {
47
- const segments = path.split('/').filter(Boolean)
48
-
49
- // Remove existing locale if present
50
- const firstSegment = segments[0]
51
- if (firstSegment && locales.includes(firstSegment)) {
52
- segments.shift()
53
- }
54
-
55
- // Add new locale if not default or if default should be included
56
- if (newLocale !== defaultLocale || defaultLocale === undefined) {
57
- segments.unshift(newLocale)
58
- }
59
-
60
- return `/${segments.join('/')}`
61
- }
62
-
63
- /**
64
- * Localize path with locale prefix
65
- */
66
- export function localizePath(path: string, locale: string, locales: string[] = [], defaultLocale?: string): string {
67
- const cleanPath = path.replace(/^\//, '').replace(/\/$/, '') || ''
68
- const segments = cleanPath.split('/').filter(Boolean)
69
-
70
- // Remove existing locale if present
71
- const firstSegment = segments[0]
72
- if (firstSegment && locales.includes(firstSegment)) {
73
- segments.shift()
74
- }
75
-
76
- // Add locale if not default or if default should be included
77
- if (locale !== defaultLocale || defaultLocale === undefined) {
78
- segments.unshift(locale)
79
- }
80
-
81
- return `/${segments.join('/')}`
82
- }
83
-
84
- /**
85
- * Remove locale from path
86
- */
87
- export function removeLocaleFromPath(path: string, locales: string[] = []): string {
88
- const segments = path.split('/').filter(Boolean)
89
- const firstSegment = segments[0]
90
- if (firstSegment && locales.includes(firstSegment)) {
91
- segments.shift()
92
- }
93
- return `/${segments.join('/')}`
94
- }