@edgekits/astro-i18n 0.1.7 → 0.2.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/CHANGELOG.md +36 -0
- package/README.md +98 -27
- package/dist/integration/index.d.ts +25 -4
- package/dist/integration/index.js +35 -9
- package/dist/integration/vite-plugin.d.ts +1 -1
- package/dist/types.d.ts +7 -55
- package/dist/types.js +1 -44
- package/package.json +7 -17
- package/src/components/LanguageSwitcher.astro +89 -91
- package/src/config.entry.ts +5 -36
- package/src/constants.ts +5 -3
- package/src/cookie-storage.ts +19 -25
- package/src/fetcher.ts +3 -304
- package/src/format.ts +3 -107
- package/src/instance.ts +44 -0
- package/src/integration/index.ts +38 -9
- package/src/integration/vite-plugin.ts +1 -1
- package/src/middleware/i18n.ts +232 -114
- package/src/resolve-locale.ts +7 -39
- package/src/runtime.ts +2 -2
- package/src/schema.ts +3 -11
- package/src/translations-keys.ts +7 -38
- package/src/types.ts +16 -116
- package/dist/cli/generate.d.ts +0 -50
- package/dist/cli/generate.js +0 -160
- package/dist/cli/hashes.d.ts +0 -19
- package/dist/cli/hashes.js +0 -42
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -368
- package/dist/cli/load-config.d.ts +0 -7
- package/dist/cli/load-config.js +0 -19
- package/dist/cli/purge.d.ts +0 -29
- package/dist/cli/purge.js +0 -94
- package/dist/cli/wrangler.d.ts +0 -10
- package/dist/cli/wrangler.js +0 -71
- package/dist/key-format.d.ts +0 -8
- package/dist/key-format.js +0 -15
- package/src/accept-language.ts +0 -56
- package/src/assemble.ts +0 -29
- package/src/cli/generate.ts +0 -205
- package/src/cli/hashes.ts +0 -66
- package/src/cli/index.ts +0 -467
- package/src/cli/load-config.ts +0 -26
- package/src/cli/purge.ts +0 -122
- package/src/cli/wrangler.ts +0 -96
- package/src/deep-merge.ts +0 -38
- package/src/key-format.ts +0 -17
- package/src/locale-fallback.ts +0 -58
- package/src/markdown-path.ts +0 -31
- package/src/parse-boolean.ts +0 -20
package/src/cli/wrangler.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import { createRequire } from 'node:module'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser'
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Wrangler config reader
|
|
9
|
-
// Parses wrangler.jsonc to extract KV namespace IDs and vars by key.
|
|
10
|
-
// Single source of truth — no hardcoded IDs or zone IDs in package.json.
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
export const KV_BINDING = 'TRANSLATIONS'
|
|
14
|
-
|
|
15
|
-
function readWranglerConfig(root: string): Record<string, unknown> {
|
|
16
|
-
const wranglerConfigPath = path.join(root, 'wrangler.jsonc')
|
|
17
|
-
|
|
18
|
-
if (!fs.existsSync(wranglerConfigPath)) {
|
|
19
|
-
console.error(`[i18n] wrangler.jsonc not found at ${wranglerConfigPath}`)
|
|
20
|
-
process.exit(1)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const raw = fs.readFileSync(wranglerConfigPath, 'utf8')
|
|
24
|
-
|
|
25
|
-
const errors: ParseError[] = []
|
|
26
|
-
const config = parseJsonc(raw, errors, { allowTrailingComma: true })
|
|
27
|
-
|
|
28
|
-
if (!config || typeof config !== 'object') {
|
|
29
|
-
console.error('[i18n] Failed to parse wrangler.jsonc — result is empty.')
|
|
30
|
-
for (const err of errors) {
|
|
31
|
-
const snippet = raw.slice(Math.max(0, err.offset - 10), err.offset + 20)
|
|
32
|
-
console.error(` ${printParseErrorCode(err.error)} at offset ${err.offset}: ...${JSON.stringify(snippet)}...`)
|
|
33
|
-
}
|
|
34
|
-
process.exit(1)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return config
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getKvNamespaceId(root: string, binding: string): string {
|
|
41
|
-
const config = readWranglerConfig(root)
|
|
42
|
-
const kvNamespaces = config.kv_namespaces as
|
|
43
|
-
| Array<{
|
|
44
|
-
binding: string
|
|
45
|
-
id: string
|
|
46
|
-
}>
|
|
47
|
-
| undefined
|
|
48
|
-
|
|
49
|
-
const entry = kvNamespaces?.find((kv) => kv.binding === binding)
|
|
50
|
-
|
|
51
|
-
if (!entry) {
|
|
52
|
-
console.error(`[i18n] KV binding "${binding}" not found in wrangler.jsonc`)
|
|
53
|
-
process.exit(1)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return entry.id
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function getWranglerVar(root: string, key: string): string | undefined {
|
|
60
|
-
const config = readWranglerConfig(root)
|
|
61
|
-
const vars = config.vars as Record<string, string> | undefined
|
|
62
|
-
return vars?.[key]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
// Wrangler binary
|
|
67
|
-
// The CLI runs wrangler's JavaScript entry with the current Node binary, never through
|
|
68
|
-
// a shell (no `.cmd` shim on Windows, nothing for a shell to interpret).
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* The absolute path of the project's wrangler entry (`bin.wrangler` in its package.json),
|
|
73
|
-
* resolved from `root` as the project itself would resolve it. `wrangler/package.json` is
|
|
74
|
-
* an exported subpath; `bin/` is not, hence the detour through the manifest. Throws when
|
|
75
|
-
* the project has no wrangler or its manifest declares no `bin.wrangler`.
|
|
76
|
-
*/
|
|
77
|
-
export function resolveWranglerBin(root: string): string {
|
|
78
|
-
const requireFromRoot = createRequire(path.join(root, 'package.json'))
|
|
79
|
-
|
|
80
|
-
let manifestPath: string
|
|
81
|
-
try {
|
|
82
|
-
manifestPath = requireFromRoot.resolve('wrangler/package.json')
|
|
83
|
-
} catch {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`wrangler is not installed in ${root}. The CLI runs the project's own wrangler: install it there (npm i -D wrangler).`,
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { bin?: string | Record<string, string> }
|
|
90
|
-
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.wrangler
|
|
91
|
-
if (!bin) {
|
|
92
|
-
throw new Error(`wrangler at ${path.dirname(manifestPath)} declares no bin.wrangler in its package.json.`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return path.join(path.dirname(manifestPath), bin)
|
|
96
|
-
}
|
package/src/deep-merge.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
2
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
// Keys that would reach an object's prototype instead of its own properties. JSON.parse
|
|
6
|
-
// creates `__proto__` as an own key, so a KV value could carry one.
|
|
7
|
-
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Deep-merge two dictionaries.
|
|
11
|
-
* - `base` provides known structure (fallback)
|
|
12
|
-
* - `overrides` can provide partial values
|
|
13
|
-
* - Both types are preserved and merged correctly
|
|
14
|
-
* - `__proto__`, `constructor` and `prototype` keys in `overrides` are skipped
|
|
15
|
-
*/
|
|
16
|
-
export function deepMerge<A extends object, B extends Partial<A>>(base: A, overrides: B): A {
|
|
17
|
-
const result: any = { ...base }
|
|
18
|
-
|
|
19
|
-
for (const key of Object.keys(overrides) as (keyof A)[]) {
|
|
20
|
-
if (UNSAFE_KEYS.has(key as string)) continue
|
|
21
|
-
|
|
22
|
-
const overrideValue = overrides[key]
|
|
23
|
-
const baseValue = base[key]
|
|
24
|
-
|
|
25
|
-
if (overrideValue === undefined) continue
|
|
26
|
-
|
|
27
|
-
// For primitives or different types — override directly
|
|
28
|
-
if (!isObject(baseValue) || !isObject(overrideValue)) {
|
|
29
|
-
result[key] = overrideValue
|
|
30
|
-
continue
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Both are objects — recurse
|
|
34
|
-
result[key] = deepMerge(baseValue as any, overrideValue as any)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return result
|
|
38
|
-
}
|
package/src/key-format.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The translation KV key and edge-cache URL formats, as pure functions of the key
|
|
3
|
-
* prefix. Shared by the runtime (`translations-keys.ts`, prefix from the virtual config)
|
|
4
|
-
* and the CLI (prefix from the loaded config), so the key a value is stored under and
|
|
5
|
-
* the URL a purge targets can never drift apart. Pinned by `test/i18n-keys.test.ts`.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Format: <keyPrefix>:<namespace>:<locale>
|
|
9
|
-
export function formatTranslationKvKey(keyPrefix: string, locale: string, ns: string): string {
|
|
10
|
-
return `${keyPrefix}:${ns}:${locale}`
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Format: https://<keyPrefix>/<encodeURIComponent(i18n:<locale>:<namespace>)>
|
|
14
|
-
export function formatTranslationCacheUrl(keyPrefix: string, locale: string, ns: string): string {
|
|
15
|
-
const cacheId = `i18n:${locale}:${ns}`
|
|
16
|
-
return `https://${keyPrefix}/${encodeURIComponent(cacheId)}`
|
|
17
|
-
}
|
package/src/locale-fallback.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The middleware's fallback cascade for a request whose URL carries no valid locale,
|
|
3
|
-
* as a pure function: the request-bound inputs arrive as getters, so each one is read
|
|
4
|
-
* exactly when, and only if, the cascade reaches it (same split as `env` in the fetcher).
|
|
5
|
-
*
|
|
6
|
-
* Order (unchanged from the middleware):
|
|
7
|
-
* 1. the lang cookie, if valid;
|
|
8
|
-
* 2. the Accept-Language header (the middleware passes the request header): each tag in
|
|
9
|
-
* `q` order, full tag, then its language part ('es-mx' -> 'es'); the first tag that
|
|
10
|
-
* names a supported locale wins (accept-language.ts);
|
|
11
|
-
* 3. ONLY when no tag matched (or there is no header): the Cloudflare geo-IP country,
|
|
12
|
-
* through the project's `countryToLocale` table (skipped without one);
|
|
13
|
-
* 4. the default locale.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { matchAcceptLanguage } from './accept-language'
|
|
17
|
-
|
|
18
|
-
export type FallbackInputs<L extends string> = {
|
|
19
|
-
cookieLocale: () => L | null
|
|
20
|
-
acceptLanguage: () => string | null
|
|
21
|
-
country: () => unknown
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type FallbackOptions<L extends string> = {
|
|
25
|
-
// Returns the locale when the value is a supported one, otherwise null.
|
|
26
|
-
parseLocale: (value: unknown) => L | null
|
|
27
|
-
defaultLocale: L
|
|
28
|
-
countryToLocale?: Readonly<Record<string, string>>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function resolveFallbackLocale<L extends string>(inputs: FallbackInputs<L>, options: FallbackOptions<L>): L {
|
|
32
|
-
const cookieLocale = inputs.cookieLocale()
|
|
33
|
-
if (cookieLocale) return cookieLocale
|
|
34
|
-
|
|
35
|
-
const browser = matchAcceptLanguage(inputs.acceptLanguage(), options.parseLocale)
|
|
36
|
-
if (browser) return browser
|
|
37
|
-
|
|
38
|
-
if (options.countryToLocale) {
|
|
39
|
-
// 3) Cloudflare Geo-IP Strategy
|
|
40
|
-
// cf is undefined in local dev unless simulated
|
|
41
|
-
const geo = options.parseLocale(mapCountryToLocale(inputs.country(), options.countryToLocale))
|
|
42
|
-
if (geo) return geo
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return options.defaultLocale
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Maps a Cloudflare country code (`request.cf.country`) to a locale through the
|
|
50
|
-
* project's table. Returns undefined if the country is not in it.
|
|
51
|
-
*/
|
|
52
|
-
export function mapCountryToLocale(country: unknown, table: Readonly<Record<string, string>>): string | undefined {
|
|
53
|
-
// Safety: In dev mode (localhost), 'country' might be null or undefined.
|
|
54
|
-
if (typeof country !== 'string') return undefined
|
|
55
|
-
|
|
56
|
-
// Cloudflare always sends uppercase (e.g., 'US', 'DE'), but safety first.
|
|
57
|
-
return table[country.toUpperCase()]
|
|
58
|
-
}
|
package/src/markdown-path.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The middleware's `.md` normalisation, as a pure function. Markdown endpoints
|
|
3
|
-
* (`/de/blog/x.md/`, `/blog/x.md`) are served in one locale, the project's
|
|
4
|
-
* `markdownLocale`; this returns the single redirect target for such a path, or null
|
|
5
|
-
* when there is nothing to redirect:
|
|
6
|
-
* - not a `.md` path, or the project has no `markdownLocale`: null (the middleware
|
|
7
|
-
* lets the request through untouched);
|
|
8
|
-
* - a leading segment that is a locale is replaced, any other leading segment is kept
|
|
9
|
-
* (`/foo.md` -> `/<markdownLocale>/foo.md`, never the homepage);
|
|
10
|
-
* - a trailing slash is dropped, in the same redirect;
|
|
11
|
-
* - a path already in canonical form: null.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export const MARKDOWN_PATH = /\.md\/?$/
|
|
15
|
-
|
|
16
|
-
export type MarkdownOptions = {
|
|
17
|
-
// True when the segment is a supported locale.
|
|
18
|
-
isLocale: (segment: string) => boolean
|
|
19
|
-
markdownLocale?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function markdownRedirectTarget(pathname: string, options: MarkdownOptions): string | null {
|
|
23
|
-
if (!options.markdownLocale || !MARKDOWN_PATH.test(pathname)) return null
|
|
24
|
-
|
|
25
|
-
const segments = pathname.split('/').filter(Boolean)
|
|
26
|
-
const first = segments[0]
|
|
27
|
-
const contentSegments = first !== undefined && options.isLocale(first) ? segments.slice(1) : segments
|
|
28
|
-
const target = `/${options.markdownLocale}/${contentSegments.join('/')}`
|
|
29
|
-
|
|
30
|
-
return target === pathname ? null : target
|
|
31
|
-
}
|
package/src/parse-boolean.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Engine-owned copy of src/utils/shared/parse-boolean.ts: the package ships its own.
|
|
2
|
-
// The project copy stays for its other consumers (seo/utils.ts, htmlCacheMiddleware.ts).
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Parse a boolean flag from Wrangler-env string.
|
|
6
|
-
*
|
|
7
|
-
* Accepts:
|
|
8
|
-
* "1", "true", "on" → true
|
|
9
|
-
* "0", "false", "off" → false
|
|
10
|
-
* undefined → defaultValue
|
|
11
|
-
*/
|
|
12
|
-
export function parseBooleanFlag(value: string | undefined, defaultValue: boolean): boolean {
|
|
13
|
-
if (value === undefined) return defaultValue
|
|
14
|
-
|
|
15
|
-
const v = value.toLowerCase()
|
|
16
|
-
if (v === '1' || v === 'true' || v === 'on') return true
|
|
17
|
-
if (v === '0' || v === 'false' || v === 'off') return false
|
|
18
|
-
|
|
19
|
-
return defaultValue
|
|
20
|
-
}
|