@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +98 -27
  3. package/dist/integration/index.d.ts +25 -4
  4. package/dist/integration/index.js +35 -9
  5. package/dist/integration/vite-plugin.d.ts +1 -1
  6. package/dist/types.d.ts +7 -55
  7. package/dist/types.js +1 -44
  8. package/package.json +7 -17
  9. package/src/components/LanguageSwitcher.astro +89 -91
  10. package/src/config.entry.ts +5 -36
  11. package/src/constants.ts +5 -3
  12. package/src/cookie-storage.ts +19 -25
  13. package/src/fetcher.ts +3 -304
  14. package/src/format.ts +3 -107
  15. package/src/instance.ts +44 -0
  16. package/src/integration/index.ts +38 -9
  17. package/src/integration/vite-plugin.ts +1 -1
  18. package/src/middleware/i18n.ts +232 -114
  19. package/src/resolve-locale.ts +7 -39
  20. package/src/runtime.ts +2 -2
  21. package/src/schema.ts +3 -11
  22. package/src/translations-keys.ts +7 -38
  23. package/src/types.ts +16 -116
  24. package/dist/cli/generate.d.ts +0 -50
  25. package/dist/cli/generate.js +0 -160
  26. package/dist/cli/hashes.d.ts +0 -19
  27. package/dist/cli/hashes.js +0 -42
  28. package/dist/cli/index.d.ts +0 -2
  29. package/dist/cli/index.js +0 -368
  30. package/dist/cli/load-config.d.ts +0 -7
  31. package/dist/cli/load-config.js +0 -19
  32. package/dist/cli/purge.d.ts +0 -29
  33. package/dist/cli/purge.js +0 -94
  34. package/dist/cli/wrangler.d.ts +0 -10
  35. package/dist/cli/wrangler.js +0 -71
  36. package/dist/key-format.d.ts +0 -8
  37. package/dist/key-format.js +0 -15
  38. package/src/accept-language.ts +0 -56
  39. package/src/assemble.ts +0 -29
  40. package/src/cli/generate.ts +0 -205
  41. package/src/cli/hashes.ts +0 -66
  42. package/src/cli/index.ts +0 -467
  43. package/src/cli/load-config.ts +0 -26
  44. package/src/cli/purge.ts +0 -122
  45. package/src/cli/wrangler.ts +0 -96
  46. package/src/deep-merge.ts +0 -38
  47. package/src/key-format.ts +0 -17
  48. package/src/locale-fallback.ts +0 -58
  49. package/src/markdown-path.ts +0 -31
  50. package/src/parse-boolean.ts +0 -20
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }