@replohq/sdk 0.6.1 → 0.7.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/_vendor/replo-utils/lib/url.d.ts +5 -0
- package/_vendor/schemas/routing/locale.d.ts +45 -0
- package/_vendor/schemas/routing/locale.mjs +95 -0
- package/_vendor/schemas/routing/localeNegotiation.d.ts +87 -0
- package/_vendor/schemas/routing/localeNegotiation.mjs +141 -0
- package/_vendor/schemas/routing/rules.mjs +1 -1
- package/lib/buildMetadata.js +3 -3
- package/package.json +14 -4
- package/routing/evaluate.d.ts +3 -1
- package/routing/evaluate.js +150 -6
- package/routing/evaluate.js.map +3 -3
- package/routing/geo.d.ts +9 -0
- package/routing/geo.js +17 -0
- package/routing/geo.js.map +7 -0
- package/routing/locale.d.ts +13 -0
- package/routing/locale.js +13 -0
- package/routing/locale.js.map +7 -0
|
@@ -55,3 +55,8 @@ export declare const isValidRoutePath: (path: string) => boolean;
|
|
|
55
55
|
* relative value such as a site path.
|
|
56
56
|
*/
|
|
57
57
|
export declare function parseUrl(value: string, base?: string): URL | null;
|
|
58
|
+
/**
|
|
59
|
+
* File extension from a URL's path, lowercased, defaulting to "jpg" for
|
|
60
|
+
* extensionless paths (product imagery is the primary consumer).
|
|
61
|
+
*/
|
|
62
|
+
export declare function fileExtensionFromUrl(url: string): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ReploError } from "../errors";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export declare class LocaleRoutingValidationError extends ReploError {
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Set by the runtime when a visitor explicitly picks a locale (a language
|
|
7
|
+
* switcher link, or landing on a locale-prefixed URL). Negotiation reads it
|
|
8
|
+
* ahead of any detection, so an explicit choice always wins.
|
|
9
|
+
*/
|
|
10
|
+
export declare const REPLO_LOCALE_COOKIE = "replo-locale";
|
|
11
|
+
/**
|
|
12
|
+
* Overrides the visitor's IP-derived country for testing geo behavior without
|
|
13
|
+
* a VPN — works identically in dev sandboxes and on the published site. At
|
|
14
|
+
* worst it lets a visitor pick which locale they see, which the locale cookie
|
|
15
|
+
* already allows deliberately.
|
|
16
|
+
*/
|
|
17
|
+
export declare const REPLO_COUNTRY_OVERRIDE_COOKIE = "replo-country-override";
|
|
18
|
+
export declare function canonicalizeLocaleTag(tag: string): string | null;
|
|
19
|
+
export declare const DOMAIN_HOSTNAME: RegExp;
|
|
20
|
+
export declare const localeRoutingConfigSchema: z.ZodObject<{
|
|
21
|
+
locales: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
22
|
+
defaultLocale: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
23
|
+
detection: z.ZodDefault<z.ZodEnum<{
|
|
24
|
+
country: "country";
|
|
25
|
+
language: "language";
|
|
26
|
+
}>>;
|
|
27
|
+
urlMode: z.ZodDefault<z.ZodEnum<{
|
|
28
|
+
prefixed: "prefixed";
|
|
29
|
+
hidden: "hidden";
|
|
30
|
+
}>>;
|
|
31
|
+
domains: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
32
|
+
locales: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
33
|
+
defaultLocale: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
34
|
+
}, z.core.$strict>>>;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export type LocaleRoutingConfig = z.infer<typeof localeRoutingConfigSchema>;
|
|
37
|
+
export type LocaleRoutingConfigInput = z.input<typeof localeRoutingConfigSchema>;
|
|
38
|
+
export type LocaleDetection = LocaleRoutingConfig["detection"];
|
|
39
|
+
export type LocaleUrlMode = LocaleRoutingConfig["urlMode"];
|
|
40
|
+
/**
|
|
41
|
+
* Validates a locale routing config, returning it with every tag
|
|
42
|
+
* canonicalized and defaults applied. Throws `LocaleRoutingValidationError`
|
|
43
|
+
* carrying the first failing check's message.
|
|
44
|
+
*/
|
|
45
|
+
export declare function reploLocaleRouting(input: LocaleRoutingConfigInput): LocaleRoutingConfig;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ReploError } from '../../../chunk-5M7VU32I.mjs';
|
|
2
|
+
import '../../../chunk-UJCSKKID.mjs';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
var LocaleRoutingValidationError = class extends ReploError {
|
|
6
|
+
};
|
|
7
|
+
var REPLO_LOCALE_COOKIE = "replo-locale";
|
|
8
|
+
var REPLO_COUNTRY_OVERRIDE_COOKIE = "replo-country-override";
|
|
9
|
+
var CANONICAL_LOCALE_TAG = /^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|\d{3}))?$/;
|
|
10
|
+
function canonicalizeLocaleTag(tag) {
|
|
11
|
+
let canonical;
|
|
12
|
+
try {
|
|
13
|
+
canonical = Intl.getCanonicalLocales(tag)[0];
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (!canonical || !CANONICAL_LOCALE_TAG.test(canonical)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return canonical;
|
|
21
|
+
}
|
|
22
|
+
var localeTagSchema = z.string().transform((tag, context) => {
|
|
23
|
+
const canonical = canonicalizeLocaleTag(tag);
|
|
24
|
+
if (canonical === null) {
|
|
25
|
+
context.addIssue({
|
|
26
|
+
code: "custom",
|
|
27
|
+
message: `"${tag}" is not a locale tag \u2014 use a BCP 47 tag like "en", "fr-CA", or "zh-Hant"`
|
|
28
|
+
});
|
|
29
|
+
return z.NEVER;
|
|
30
|
+
}
|
|
31
|
+
return canonical;
|
|
32
|
+
});
|
|
33
|
+
var DOMAIN_HOSTNAME = /^[a-z0-9]([a-z0-9-]{0,62})?(\.[a-z0-9]([a-z0-9-]{0,62})?)+$/;
|
|
34
|
+
var domainScopeSchema = z.object({
|
|
35
|
+
locales: z.array(localeTagSchema).min(1),
|
|
36
|
+
defaultLocale: localeTagSchema.optional()
|
|
37
|
+
}).strict();
|
|
38
|
+
var localeRoutingConfigSchema = z.object({
|
|
39
|
+
locales: z.array(localeTagSchema).min(2),
|
|
40
|
+
defaultLocale: localeTagSchema,
|
|
41
|
+
detection: z.enum(["language", "country"]).default("language"),
|
|
42
|
+
urlMode: z.enum(["prefixed", "hidden"]).default("prefixed"),
|
|
43
|
+
domains: z.record(z.string(), domainScopeSchema).optional()
|
|
44
|
+
}).strict().superRefine((config, context) => {
|
|
45
|
+
const configured = new Set(config.locales);
|
|
46
|
+
if (configured.size !== config.locales.length) {
|
|
47
|
+
context.addIssue({
|
|
48
|
+
code: "custom",
|
|
49
|
+
path: ["locales"],
|
|
50
|
+
message: "locales must not repeat after canonicalization"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!configured.has(config.defaultLocale)) {
|
|
54
|
+
context.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
path: ["defaultLocale"],
|
|
57
|
+
message: `defaultLocale "${config.defaultLocale}" must be one of locales`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const [hostname, scope] of Object.entries(config.domains ?? {})) {
|
|
61
|
+
if (!DOMAIN_HOSTNAME.test(hostname)) {
|
|
62
|
+
context.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: ["domains", hostname],
|
|
65
|
+
message: `"${hostname}" must be a bare lowercase hostname like "example.com" \u2014 no scheme, path, or port`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const locale of scope.locales) {
|
|
69
|
+
if (!configured.has(locale)) {
|
|
70
|
+
context.addIssue({
|
|
71
|
+
code: "custom",
|
|
72
|
+
path: ["domains", hostname, "locales"],
|
|
73
|
+
message: `"${locale}" is not in the top-level locales list`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (scope.defaultLocale !== void 0 && !scope.locales.includes(scope.defaultLocale)) {
|
|
78
|
+
context.addIssue({
|
|
79
|
+
code: "custom",
|
|
80
|
+
path: ["domains", hostname, "defaultLocale"],
|
|
81
|
+
message: `defaultLocale "${scope.defaultLocale}" must be one of this domain's locales`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
function reploLocaleRouting(input) {
|
|
87
|
+
const parsed = localeRoutingConfigSchema.safeParse(input);
|
|
88
|
+
if (parsed.success) {
|
|
89
|
+
return parsed.data;
|
|
90
|
+
}
|
|
91
|
+
const message = parsed.error.issues[0]?.message ?? "invalid locale routing config";
|
|
92
|
+
throw new LocaleRoutingValidationError({ message });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { DOMAIN_HOSTNAME, LocaleRoutingValidationError, REPLO_COUNTRY_OVERRIDE_COOKIE, REPLO_LOCALE_COOKIE, canonicalizeLocaleTag, localeRoutingConfigSchema, reploLocaleRouting };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { LocaleRoutingConfig } from "./locale";
|
|
2
|
+
/** The locales one request negotiates within, after domain scoping. */
|
|
3
|
+
export interface LocaleScope {
|
|
4
|
+
locales: string[];
|
|
5
|
+
defaultLocale: string;
|
|
6
|
+
}
|
|
7
|
+
export interface NegotiatedLocale {
|
|
8
|
+
locale: string;
|
|
9
|
+
source: "cookie" | "country" | "language" | "default";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The scope a request's Host header lands in. A host with no domain entry —
|
|
13
|
+
* including every dev sandbox and preview host — gets the global scope, which
|
|
14
|
+
* keeps domain configs inert anywhere the real hostnames don't resolve.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveLocaleScope({ host, config, }: {
|
|
17
|
+
host: string | null;
|
|
18
|
+
config: LocaleRoutingConfig;
|
|
19
|
+
}): LocaleScope;
|
|
20
|
+
/**
|
|
21
|
+
* The tags of an `Accept-Language` header, highest preference first. `*` is
|
|
22
|
+
* dropped — matching anything and matching nothing both fall through to the
|
|
23
|
+
* default locale — and so is any tag `matchBestFitLocale` would choke on, so
|
|
24
|
+
* one garbage entry never voids the visitor's real preferences.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseAcceptLanguage(header: string | null): string[];
|
|
27
|
+
/**
|
|
28
|
+
* The best supported locale for an ordered list of requested tags, or null
|
|
29
|
+
* when no requested tag relates to any supported locale. Supported order
|
|
30
|
+
* breaks ties, so the config's own ordering expresses preference.
|
|
31
|
+
*/
|
|
32
|
+
export declare function matchLocale({ requested, supported, }: {
|
|
33
|
+
requested: readonly string[];
|
|
34
|
+
supported: readonly string[];
|
|
35
|
+
}): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* One request's locale, from the strongest signal available: an explicit
|
|
38
|
+
* cookie choice, then the detection strategy, then the scope default. Country
|
|
39
|
+
* detection derives its candidates from the locales' own region subtags —
|
|
40
|
+
* `CA` finds `en-CA` and `fr-CA` — and breaks multilingual-country ties with
|
|
41
|
+
* `Accept-Language` among just those candidates. A country matching no locale
|
|
42
|
+
* falls through to language detection rather than the default, so a German
|
|
43
|
+
* visitor of a `["en", "fr"]` site still gets a language they asked for.
|
|
44
|
+
*/
|
|
45
|
+
export declare function negotiateLocale({ scope, detection, cookieLocale, countryCode, acceptLanguageHeader, }: {
|
|
46
|
+
scope: LocaleScope;
|
|
47
|
+
detection: LocaleRoutingConfig["detection"];
|
|
48
|
+
cookieLocale: string | null;
|
|
49
|
+
countryCode: string | null;
|
|
50
|
+
acceptLanguageHeader: string | null;
|
|
51
|
+
}): NegotiatedLocale;
|
|
52
|
+
export interface PathLocale {
|
|
53
|
+
locale: string;
|
|
54
|
+
/** The prefix segment as it appeared in the URL, casing included. */
|
|
55
|
+
pathSegment: string;
|
|
56
|
+
/** The pathname with the locale prefix removed; always starts with `/`. */
|
|
57
|
+
logicalPath: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The configured locale a pathname's first segment names, or null. Matching
|
|
61
|
+
* is case-insensitive — `/en-ca/about` still identifies `en-CA` — and the
|
|
62
|
+
* caller decides whether a non-canonical spelling deserves a redirect.
|
|
63
|
+
*/
|
|
64
|
+
export declare function getLocaleFromPath({ pathname, locales, }: {
|
|
65
|
+
pathname: string;
|
|
66
|
+
locales: readonly string[];
|
|
67
|
+
}): PathLocale | null;
|
|
68
|
+
/** `/about` under `fr-CA` is `/fr-CA/about`; the root is `/fr-CA` itself. */
|
|
69
|
+
export declare function withLocalePrefix({ locale, path, }: {
|
|
70
|
+
locale: string;
|
|
71
|
+
path: string;
|
|
72
|
+
}): string;
|
|
73
|
+
/**
|
|
74
|
+
* The URL of a logical path in another locale — the one primitive a language
|
|
75
|
+
* switcher and hreflang alternates need. Within the current host's scope (or
|
|
76
|
+
* with no domains configured) it is the locale-prefixed path; a locale served
|
|
77
|
+
* by a different domain gets an absolute URL there, dropping the prefix when
|
|
78
|
+
* that domain serves only the one locale. Hidden-mode sites use the prefixed
|
|
79
|
+
* form too: the runtime converts an explicit locale prefix into a cookie plus
|
|
80
|
+
* a clean-URL redirect on arrival.
|
|
81
|
+
*/
|
|
82
|
+
export declare function localeUrl({ locale, path, config, currentHost, }: {
|
|
83
|
+
locale: string;
|
|
84
|
+
path: string;
|
|
85
|
+
config: LocaleRoutingConfig;
|
|
86
|
+
currentHost?: string | null;
|
|
87
|
+
}): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import '../../../chunk-UJCSKKID.mjs';
|
|
2
|
+
import { match } from '@formatjs/intl-localematcher';
|
|
3
|
+
import Negotiator from 'negotiator';
|
|
4
|
+
|
|
5
|
+
function resolveLocaleScope({
|
|
6
|
+
host,
|
|
7
|
+
config
|
|
8
|
+
}) {
|
|
9
|
+
const hostname = host?.trim().toLowerCase().split(":")[0] ?? "";
|
|
10
|
+
const scope = config.domains?.[hostname];
|
|
11
|
+
if (!scope) {
|
|
12
|
+
return { locales: config.locales, defaultLocale: config.defaultLocale };
|
|
13
|
+
}
|
|
14
|
+
const firstScopeLocale = scope.locales[0] ?? config.defaultLocale;
|
|
15
|
+
return {
|
|
16
|
+
locales: scope.locales,
|
|
17
|
+
defaultLocale: scope.defaultLocale ?? (scope.locales.includes(config.defaultLocale) ? config.defaultLocale : firstScopeLocale)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function parseAcceptLanguage(header) {
|
|
21
|
+
if (!header) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const languages = new Negotiator({
|
|
25
|
+
headers: { "accept-language": header }
|
|
26
|
+
}).languages();
|
|
27
|
+
return languages.filter((tag) => tag !== "*" && isStructurallyValidTag(tag));
|
|
28
|
+
}
|
|
29
|
+
function matchLocale({
|
|
30
|
+
requested,
|
|
31
|
+
supported
|
|
32
|
+
}) {
|
|
33
|
+
const wellFormed = requested.filter(isStructurallyValidTag);
|
|
34
|
+
if (wellFormed.length === 0 || supported.length === 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const result = match(wellFormed, [...supported], "und");
|
|
38
|
+
return supported.includes(result) ? result : null;
|
|
39
|
+
}
|
|
40
|
+
function negotiateLocale({
|
|
41
|
+
scope,
|
|
42
|
+
detection,
|
|
43
|
+
cookieLocale,
|
|
44
|
+
countryCode,
|
|
45
|
+
acceptLanguageHeader
|
|
46
|
+
}) {
|
|
47
|
+
const fromCookie = cookieLocale ? findTagInsensitive({ tag: cookieLocale, supported: scope.locales }) : null;
|
|
48
|
+
if (fromCookie) {
|
|
49
|
+
return { locale: fromCookie, source: "cookie" };
|
|
50
|
+
}
|
|
51
|
+
const requested = parseAcceptLanguage(acceptLanguageHeader);
|
|
52
|
+
if (detection === "country" && countryCode) {
|
|
53
|
+
const region = countryCode.trim().toUpperCase();
|
|
54
|
+
const candidates = scope.locales.filter(
|
|
55
|
+
(locale) => localeRegion(locale) === region
|
|
56
|
+
);
|
|
57
|
+
const [firstCandidate] = candidates;
|
|
58
|
+
if (firstCandidate !== void 0) {
|
|
59
|
+
return {
|
|
60
|
+
locale: matchLocale({ requested, supported: candidates }) ?? firstCandidate,
|
|
61
|
+
source: "country"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const fromLanguage = matchLocale({ requested, supported: scope.locales });
|
|
66
|
+
if (fromLanguage) {
|
|
67
|
+
return { locale: fromLanguage, source: "language" };
|
|
68
|
+
}
|
|
69
|
+
return { locale: scope.defaultLocale, source: "default" };
|
|
70
|
+
}
|
|
71
|
+
function getLocaleFromPath({
|
|
72
|
+
pathname,
|
|
73
|
+
locales
|
|
74
|
+
}) {
|
|
75
|
+
const [, firstSegment = "", ...restSegments] = pathname.split("/");
|
|
76
|
+
const locale = findTagInsensitive({ tag: firstSegment, supported: locales });
|
|
77
|
+
if (!locale) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const rest = restSegments.join("/");
|
|
81
|
+
return {
|
|
82
|
+
locale,
|
|
83
|
+
pathSegment: firstSegment,
|
|
84
|
+
logicalPath: `/${rest}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function withLocalePrefix({
|
|
88
|
+
locale,
|
|
89
|
+
path
|
|
90
|
+
}) {
|
|
91
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
92
|
+
return normalized === "/" ? `/${locale}` : `/${locale}${normalized}`;
|
|
93
|
+
}
|
|
94
|
+
function localeUrl({
|
|
95
|
+
locale,
|
|
96
|
+
path,
|
|
97
|
+
config,
|
|
98
|
+
currentHost
|
|
99
|
+
}) {
|
|
100
|
+
const currentScope = resolveLocaleScope({
|
|
101
|
+
host: currentHost ?? null,
|
|
102
|
+
config
|
|
103
|
+
});
|
|
104
|
+
if (!config.domains || currentScope.locales.includes(locale)) {
|
|
105
|
+
return withLocalePrefix({ locale, path });
|
|
106
|
+
}
|
|
107
|
+
const hosting = Object.entries(config.domains).find(([, scope2]) => {
|
|
108
|
+
return scope2.locales.includes(locale);
|
|
109
|
+
});
|
|
110
|
+
if (!hosting) {
|
|
111
|
+
return withLocalePrefix({ locale, path });
|
|
112
|
+
}
|
|
113
|
+
const [hostname, scope] = hosting;
|
|
114
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
115
|
+
const pathPart = scope.locales.length === 1 ? normalized : withLocalePrefix({ locale, path: normalized });
|
|
116
|
+
return `https://${hostname}${pathPart === "/" ? "" : pathPart}`;
|
|
117
|
+
}
|
|
118
|
+
function findTagInsensitive({
|
|
119
|
+
tag,
|
|
120
|
+
supported
|
|
121
|
+
}) {
|
|
122
|
+
const lowered = tag.toLowerCase();
|
|
123
|
+
return supported.find((candidate) => candidate.toLowerCase() === lowered) ?? null;
|
|
124
|
+
}
|
|
125
|
+
function isStructurallyValidTag(tag) {
|
|
126
|
+
try {
|
|
127
|
+
Intl.getCanonicalLocales(tag);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function localeRegion(locale) {
|
|
134
|
+
try {
|
|
135
|
+
return new Intl.Locale(locale).region ?? null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { getLocaleFromPath, localeUrl, matchLocale, negotiateLocale, parseAcceptLanguage, resolveLocaleScope, withLocalePrefix };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ReploError } from '../../../chunk-5M7VU32I.mjs';
|
|
2
1
|
import { getParamSegment, getRouteParams, hasRouteParam, matchSource } from '../../../chunk-VPEVERWE.mjs';
|
|
2
|
+
import { ReploError } from '../../../chunk-5M7VU32I.mjs';
|
|
3
3
|
import '../../../chunk-UJCSKKID.mjs';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-08-
|
|
5
|
+
"commit": "539d866ed8d4f2a8f13f1b3bd23cc3131d96c35f",
|
|
6
|
+
"builtAt": "2026-08-20T02:24:19.564Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.7.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-08-
|
|
8
|
+
"commit": "539d866ed8d4f2a8f13f1b3bd23cc3131d96c35f",
|
|
9
|
+
"builtAt": "2026-08-20T02:24:19.564Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -209,6 +209,14 @@
|
|
|
209
209
|
"types": "./routing/rules.d.ts",
|
|
210
210
|
"default": "./routing/rules.js"
|
|
211
211
|
},
|
|
212
|
+
"./routing/locale": {
|
|
213
|
+
"types": "./routing/locale.d.ts",
|
|
214
|
+
"default": "./routing/locale.js"
|
|
215
|
+
},
|
|
216
|
+
"./routing/geo": {
|
|
217
|
+
"types": "./routing/geo.d.ts",
|
|
218
|
+
"default": "./routing/geo.js"
|
|
219
|
+
},
|
|
212
220
|
"./_vendor/schemas/loaderKeys": {
|
|
213
221
|
"types": "./_vendor/schemas/loaderKeys.d.ts",
|
|
214
222
|
"default": "./_vendor/schemas/loaderKeys.mjs"
|
|
@@ -216,6 +224,8 @@
|
|
|
216
224
|
"./package.json": "./package.json"
|
|
217
225
|
},
|
|
218
226
|
"dependencies": {
|
|
227
|
+
"@formatjs/intl-localematcher": "^0.8.13",
|
|
228
|
+
"negotiator": "^1.0.0",
|
|
219
229
|
"tiny-jsonc": "^1.0.2",
|
|
220
230
|
"uuid": "^11.1.0",
|
|
221
231
|
"zod": "^4.3.6"
|
package/routing/evaluate.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
|
+
import type { LocaleRoutingConfig } from "../_vendor/schemas/routing/locale";
|
|
2
3
|
import type { RedirectRule } from "../_vendor/schemas/routing/rules";
|
|
3
4
|
import { NextResponse } from "next/server";
|
|
4
|
-
export declare function evaluateRouting({ request, rules, }: {
|
|
5
|
+
export declare function evaluateRouting({ request, rules, localeRouting, }: {
|
|
5
6
|
request: NextRequest;
|
|
6
7
|
rules: readonly RedirectRule[];
|
|
8
|
+
localeRouting?: LocaleRoutingConfig;
|
|
7
9
|
}): NextResponse;
|
package/routing/evaluate.js
CHANGED
|
@@ -1,25 +1,66 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { REPLO_LOCALE_COOKIE } from "../_vendor/schemas/routing/locale.mjs";
|
|
3
|
+
import {
|
|
4
|
+
getLocaleFromPath,
|
|
5
|
+
negotiateLocale,
|
|
6
|
+
resolveLocaleScope,
|
|
7
|
+
withLocalePrefix
|
|
8
|
+
} from "../_vendor/schemas/routing/localeNegotiation.mjs";
|
|
2
9
|
import { applyParams, matchSource } from "../_vendor/schemas/routing/match.mjs";
|
|
10
|
+
import { ipCountry } from "./geo";
|
|
3
11
|
function evaluateRouting({
|
|
4
12
|
request,
|
|
5
|
-
rules
|
|
13
|
+
rules,
|
|
14
|
+
localeRouting
|
|
6
15
|
}) {
|
|
16
|
+
const localeContext = localeRouting ? getLocaleContext({ request, config: localeRouting }) : null;
|
|
7
17
|
for (const rule of rules) {
|
|
8
|
-
const response = evaluateRedirect({ request, rule });
|
|
18
|
+
const response = evaluateRedirect({ request, rule, localeContext });
|
|
9
19
|
if (response) {
|
|
10
20
|
return response;
|
|
11
21
|
}
|
|
12
22
|
}
|
|
13
|
-
return
|
|
23
|
+
return evaluateLocalePhase({ request, localeContext });
|
|
24
|
+
}
|
|
25
|
+
function getLocaleContext({
|
|
26
|
+
request,
|
|
27
|
+
config
|
|
28
|
+
}) {
|
|
29
|
+
const scope = resolveLocaleScope({
|
|
30
|
+
host: request.headers.get("host"),
|
|
31
|
+
config
|
|
32
|
+
});
|
|
33
|
+
const pathLocale = getLocaleFromPath({
|
|
34
|
+
pathname: request.nextUrl.pathname,
|
|
35
|
+
locales: config.locales
|
|
36
|
+
});
|
|
37
|
+
const negotiated = negotiateLocale({
|
|
38
|
+
scope,
|
|
39
|
+
detection: config.detection,
|
|
40
|
+
cookieLocale: request.cookies.get(REPLO_LOCALE_COOKIE)?.value ?? null,
|
|
41
|
+
countryCode: ipCountry(request),
|
|
42
|
+
acceptLanguageHeader: request.headers.get("accept-language")
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
config,
|
|
46
|
+
scope,
|
|
47
|
+
urlMode: scope.locales.length === 1 ? "hidden" : config.urlMode,
|
|
48
|
+
pathLocale,
|
|
49
|
+
negotiated
|
|
50
|
+
};
|
|
14
51
|
}
|
|
15
52
|
function evaluateRedirect({
|
|
16
53
|
request,
|
|
17
|
-
rule
|
|
54
|
+
rule,
|
|
55
|
+
localeContext
|
|
18
56
|
}) {
|
|
19
57
|
const params = matchSource({
|
|
20
58
|
source: rule.source,
|
|
21
59
|
pathname: request.nextUrl.pathname
|
|
22
|
-
})
|
|
60
|
+
}) ?? (localeContext?.pathLocale ? matchSource({
|
|
61
|
+
source: rule.source,
|
|
62
|
+
pathname: localeContext.pathLocale.logicalPath
|
|
63
|
+
}) : null);
|
|
23
64
|
if (!params) {
|
|
24
65
|
return null;
|
|
25
66
|
}
|
|
@@ -30,11 +71,114 @@ function evaluateRedirect({
|
|
|
30
71
|
if (url.origin === request.nextUrl.origin && !url.search) {
|
|
31
72
|
url.search = request.nextUrl.search;
|
|
32
73
|
}
|
|
74
|
+
if (localeContext && url.origin === request.nextUrl.origin) {
|
|
75
|
+
applyLocaleToDestination({ url, localeContext });
|
|
76
|
+
}
|
|
33
77
|
const redirectsToCurrentRequest = url.origin === request.nextUrl.origin && normalizePath(url.pathname) === normalizePath(request.nextUrl.pathname) && url.search === request.nextUrl.search;
|
|
34
78
|
if (redirectsToCurrentRequest) {
|
|
35
79
|
return NextResponse.next();
|
|
36
80
|
}
|
|
37
|
-
|
|
81
|
+
const response = NextResponse.redirect(url, rule.status);
|
|
82
|
+
if (localeContext?.pathLocale && localeContext.urlMode === "hidden" && url.origin === request.nextUrl.origin && localeContext.scope.locales.includes(localeContext.pathLocale.locale)) {
|
|
83
|
+
setLocaleCookie({ response, locale: localeContext.pathLocale.locale });
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
function applyLocaleToDestination({
|
|
88
|
+
url,
|
|
89
|
+
localeContext
|
|
90
|
+
}) {
|
|
91
|
+
if (localeContext.urlMode !== "prefixed") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (isLocaleExemptPath(url.pathname)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const destinationLocale = getLocaleFromPath({
|
|
98
|
+
pathname: url.pathname,
|
|
99
|
+
locales: localeContext.config.locales
|
|
100
|
+
});
|
|
101
|
+
if (destinationLocale) {
|
|
102
|
+
if (destinationLocale.pathSegment !== destinationLocale.locale) {
|
|
103
|
+
url.pathname = withLocalePrefix({
|
|
104
|
+
locale: destinationLocale.locale,
|
|
105
|
+
path: destinationLocale.logicalPath
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
url.pathname = withLocalePrefix({
|
|
111
|
+
locale: localeContext.pathLocale?.locale ?? localeContext.negotiated.locale,
|
|
112
|
+
path: url.pathname
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function evaluateLocalePhase({
|
|
116
|
+
request,
|
|
117
|
+
localeContext
|
|
118
|
+
}) {
|
|
119
|
+
if (!localeContext) {
|
|
120
|
+
return NextResponse.next();
|
|
121
|
+
}
|
|
122
|
+
const { pathname } = request.nextUrl;
|
|
123
|
+
if (isLocaleExemptPath(pathname)) {
|
|
124
|
+
return NextResponse.next();
|
|
125
|
+
}
|
|
126
|
+
const { urlMode, pathLocale, negotiated, scope } = localeContext;
|
|
127
|
+
if (urlMode === "prefixed") {
|
|
128
|
+
if (!pathLocale) {
|
|
129
|
+
const url2 = request.nextUrl.clone();
|
|
130
|
+
url2.pathname = withLocalePrefix({
|
|
131
|
+
locale: negotiated.locale,
|
|
132
|
+
path: pathname
|
|
133
|
+
});
|
|
134
|
+
return NextResponse.redirect(url2, 307);
|
|
135
|
+
}
|
|
136
|
+
if (pathLocale.pathSegment !== pathLocale.locale) {
|
|
137
|
+
const url2 = request.nextUrl.clone();
|
|
138
|
+
url2.pathname = withLocalePrefix({
|
|
139
|
+
locale: pathLocale.locale,
|
|
140
|
+
path: pathLocale.logicalPath
|
|
141
|
+
});
|
|
142
|
+
return NextResponse.redirect(url2, 308);
|
|
143
|
+
}
|
|
144
|
+
const response = NextResponse.next();
|
|
145
|
+
if (pathLocale.locale !== negotiated.locale && request.cookies.get(REPLO_LOCALE_COOKIE)?.value !== pathLocale.locale && scope.locales.includes(pathLocale.locale)) {
|
|
146
|
+
setLocaleCookie({ response, locale: pathLocale.locale });
|
|
147
|
+
}
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
if (pathLocale) {
|
|
151
|
+
const url2 = request.nextUrl.clone();
|
|
152
|
+
url2.pathname = pathLocale.logicalPath;
|
|
153
|
+
const response = NextResponse.redirect(url2, 307);
|
|
154
|
+
if (scope.locales.includes(pathLocale.locale)) {
|
|
155
|
+
setLocaleCookie({ response, locale: pathLocale.locale });
|
|
156
|
+
}
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
const url = request.nextUrl.clone();
|
|
160
|
+
url.pathname = withLocalePrefix({
|
|
161
|
+
locale: negotiated.locale,
|
|
162
|
+
path: pathname
|
|
163
|
+
});
|
|
164
|
+
return NextResponse.rewrite(url);
|
|
165
|
+
}
|
|
166
|
+
function isLocaleExemptPath(pathname) {
|
|
167
|
+
if (pathname === "/api" || pathname.startsWith("/api/")) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
171
|
+
return lastSegment.includes(".");
|
|
172
|
+
}
|
|
173
|
+
function setLocaleCookie({
|
|
174
|
+
response,
|
|
175
|
+
locale
|
|
176
|
+
}) {
|
|
177
|
+
response.cookies.set(REPLO_LOCALE_COOKIE, locale, {
|
|
178
|
+
path: "/",
|
|
179
|
+
maxAge: 31536e3,
|
|
180
|
+
sameSite: "lax"
|
|
181
|
+
});
|
|
38
182
|
}
|
|
39
183
|
function normalizePath(pathname) {
|
|
40
184
|
const trimmed = pathname.replace(/\/+$/, "");
|
package/routing/evaluate.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../routing/evaluate.ts"],
|
|
4
|
-
"sourcesContent": ["import type { NextRequest } from \"next/server\";\nimport type { RedirectRule } from \"schemas/routing/rules\";\n\nimport { NextResponse } from \"next/server\";\n\nimport { applyParams, matchSource } from \"schemas/routing/match\";\n\nexport function evaluateRouting({\n request,\n rules,\n}: {\n request: NextRequest;\n rules: readonly RedirectRule[];\n}): NextResponse {\n for (const rule of rules) {\n const response = evaluateRedirect({ request, rule });\n if (response) {\n return response;\n }\n }\n return
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import type { NextRequest } from \"next/server\";\nimport type { LocaleRoutingConfig } from \"schemas/routing/locale\";\nimport type {\n LocaleScope,\n NegotiatedLocale,\n PathLocale,\n} from \"schemas/routing/localeNegotiation\";\nimport type { RedirectRule } from \"schemas/routing/rules\";\n\nimport { NextResponse } from \"next/server\";\n\nimport { REPLO_LOCALE_COOKIE } from \"schemas/routing/locale\";\nimport {\n getLocaleFromPath,\n negotiateLocale,\n resolveLocaleScope,\n withLocalePrefix,\n} from \"schemas/routing/localeNegotiation\";\nimport { applyParams, matchSource } from \"schemas/routing/match\";\n\nimport { ipCountry } from \"./geo\";\n\nexport function evaluateRouting({\n request,\n rules,\n localeRouting,\n}: {\n request: NextRequest;\n rules: readonly RedirectRule[];\n localeRouting?: LocaleRoutingConfig;\n}): NextResponse {\n const localeContext = localeRouting\n ? getLocaleContext({ request, config: localeRouting })\n : null;\n for (const rule of rules) {\n const response = evaluateRedirect({ request, rule, localeContext });\n if (response) {\n return response;\n }\n }\n return evaluateLocalePhase({ request, localeContext });\n}\n\ninterface LocaleContext {\n config: LocaleRoutingConfig;\n scope: LocaleScope;\n /** A single-locale domain serves clean URLs whatever the config says. */\n urlMode: LocaleRoutingConfig[\"urlMode\"];\n pathLocale: PathLocale | null;\n negotiated: NegotiatedLocale;\n}\n\nfunction getLocaleContext({\n request,\n config,\n}: {\n request: NextRequest;\n config: LocaleRoutingConfig;\n}): LocaleContext {\n const scope = resolveLocaleScope({\n host: request.headers.get(\"host\"),\n config,\n });\n // The prefix is read against every configured locale, not just the scope's:\n // an explicitly requested out-of-scope locale is served rather than fought,\n // it just never wins negotiation or gets advertised in a cookie.\n const pathLocale = getLocaleFromPath({\n pathname: request.nextUrl.pathname,\n locales: config.locales,\n });\n const negotiated = negotiateLocale({\n scope,\n detection: config.detection,\n cookieLocale: request.cookies.get(REPLO_LOCALE_COOKIE)?.value ?? null,\n countryCode: ipCountry(request),\n acceptLanguageHeader: request.headers.get(\"accept-language\"),\n });\n return {\n config,\n scope,\n urlMode: scope.locales.length === 1 ? \"hidden\" : config.urlMode,\n pathLocale,\n negotiated,\n };\n}\n\nfunction evaluateRedirect({\n request,\n rule,\n localeContext,\n}: {\n request: NextRequest;\n rule: RedirectRule;\n localeContext: LocaleContext | null;\n}): NextResponse | null {\n // Raw first, so a locale-prefixed source targets exactly that locale; then\n // the locale-stripped path, so a rule authored against the logical path\n // covers every locale.\n const params =\n matchSource({\n source: rule.source,\n pathname: request.nextUrl.pathname,\n }) ??\n (localeContext?.pathLocale\n ? matchSource({\n source: rule.source,\n pathname: localeContext.pathLocale.logicalPath,\n })\n : null);\n if (!params) {\n return null;\n }\n const url = new URL(\n applyParams({ value: rule.destination, params }),\n request.nextUrl.origin,\n );\n if (url.origin === request.nextUrl.origin && !url.search) {\n url.search = request.nextUrl.search;\n }\n if (localeContext && url.origin === request.nextUrl.origin) {\n applyLocaleToDestination({ url, localeContext });\n }\n // A destination that lands back on the current request would loop. The rule\n // still consumes the request \u2014 later rules were authored as alternatives to\n // it, not fallbacks for it \u2014 so the page renders normally instead.\n const redirectsToCurrentRequest =\n url.origin === request.nextUrl.origin &&\n normalizePath(url.pathname) === normalizePath(request.nextUrl.pathname) &&\n url.search === request.nextUrl.search;\n if (redirectsToCurrentRequest) {\n return NextResponse.next();\n }\n const response = NextResponse.redirect(url, rule.status);\n // Hidden-mode destinations stay clean, so the locale an explicit prefix\n // carried would be lost across the hop \u2014 keep it in the cookie instead.\n if (\n localeContext?.pathLocale &&\n localeContext.urlMode === \"hidden\" &&\n url.origin === request.nextUrl.origin &&\n localeContext.scope.locales.includes(localeContext.pathLocale.locale)\n ) {\n setLocaleCookie({ response, locale: localeContext.pathLocale.locale });\n }\n return response;\n}\n\n/**\n * In prefixed mode every page lives under a locale, so an unprefixed\n * same-site destination gets one \u2014 the request's own locale when the URL\n * carried one, the negotiated locale otherwise \u2014 collapsing what would be a\n * second redirect hop. A destination that names a locale is left alone:\n * explicit always wins.\n */\nfunction applyLocaleToDestination({\n url,\n localeContext,\n}: {\n url: URL;\n localeContext: LocaleContext;\n}) {\n if (localeContext.urlMode !== \"prefixed\") {\n return;\n }\n if (isLocaleExemptPath(url.pathname)) {\n return;\n }\n const destinationLocale = getLocaleFromPath({\n pathname: url.pathname,\n locales: localeContext.config.locales,\n });\n if (destinationLocale) {\n // A non-canonically-cased prefix (`/fr-ca/x`) would otherwise 308 back to\n // the canonical spelling, re-match the rule, and loop forever \u2014 the loop\n // guard compares pathnames case-sensitively.\n if (destinationLocale.pathSegment !== destinationLocale.locale) {\n url.pathname = withLocalePrefix({\n locale: destinationLocale.locale,\n path: destinationLocale.logicalPath,\n });\n }\n return;\n }\n url.pathname = withLocalePrefix({\n locale: localeContext.pathLocale?.locale ?? localeContext.negotiated.locale,\n path: url.pathname,\n });\n}\n\nfunction evaluateLocalePhase({\n request,\n localeContext,\n}: {\n request: NextRequest;\n localeContext: LocaleContext | null;\n}): NextResponse {\n if (!localeContext) {\n return NextResponse.next();\n }\n const { pathname } = request.nextUrl;\n if (isLocaleExemptPath(pathname)) {\n return NextResponse.next();\n }\n const { urlMode, pathLocale, negotiated, scope } = localeContext;\n\n if (urlMode === \"prefixed\") {\n if (!pathLocale) {\n const url = request.nextUrl.clone();\n url.pathname = withLocalePrefix({\n locale: negotiated.locale,\n path: pathname,\n });\n // Temporary: the same URL legitimately serves different locales to\n // different visitors, so this must never cache as permanent.\n return NextResponse.redirect(url, 307);\n }\n if (pathLocale.pathSegment !== pathLocale.locale) {\n const url = request.nextUrl.clone();\n url.pathname = withLocalePrefix({\n locale: pathLocale.locale,\n path: pathLocale.logicalPath,\n });\n return NextResponse.redirect(url, 308);\n }\n const response = NextResponse.next();\n // A prefix that differs from what negotiation would pick is an explicit\n // choice \u2014 keep it sticky. A matching prefix proves nothing: the 307\n // above manufactures exactly that navigation, and persisting the guess\n // would freeze the first detection result for a year.\n if (\n pathLocale.locale !== negotiated.locale &&\n request.cookies.get(REPLO_LOCALE_COOKIE)?.value !== pathLocale.locale &&\n scope.locales.includes(pathLocale.locale)\n ) {\n setLocaleCookie({ response, locale: pathLocale.locale });\n }\n return response;\n }\n\n // Hidden mode: a locale-prefixed URL is the language-switcher mechanism \u2014\n // adopt the locale as a cookie and settle on the clean URL.\n if (pathLocale) {\n const url = request.nextUrl.clone();\n url.pathname = pathLocale.logicalPath;\n const response = NextResponse.redirect(url, 307);\n if (scope.locales.includes(pathLocale.locale)) {\n setLocaleCookie({ response, locale: pathLocale.locale });\n }\n return response;\n }\n const url = request.nextUrl.clone();\n url.pathname = withLocalePrefix({\n locale: negotiated.locale,\n path: pathname,\n });\n return NextResponse.rewrite(url);\n}\n\n// Requests for files and API routes have no locale variant; prefixing or\n// rewriting them would just 404.\nfunction isLocaleExemptPath(pathname: string): boolean {\n if (pathname === \"/api\" || pathname.startsWith(\"/api/\")) {\n return true;\n }\n const lastSegment = pathname.slice(pathname.lastIndexOf(\"/\") + 1);\n return lastSegment.includes(\".\");\n}\n\nfunction setLocaleCookie({\n response,\n locale,\n}: {\n response: NextResponse;\n locale: string;\n}) {\n response.cookies.set(REPLO_LOCALE_COOKIE, locale, {\n path: \"/\",\n maxAge: 31_536_000,\n sameSite: \"lax\",\n });\n}\n\n/** Trailing slashes carry no meaning, so `/blogs` and `/blogs/` are the same. */\nfunction normalizePath(pathname: string): string {\n const trimmed = pathname.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"/\" : trimmed;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["url"]
|
|
7
7
|
}
|
package/routing/geo.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
/**
|
|
3
|
+
* The visitor's ISO 3166-1 alpha-2 country, or null when unknown. The
|
|
4
|
+
* override cookie wins so geo behavior is testable without a VPN — in dev
|
|
5
|
+
* sandboxes, where the Cloudflare header never exists, and on the published
|
|
6
|
+
* site alike. At worst the override lets a visitor pick which locale they
|
|
7
|
+
* see, which the locale cookie already allows deliberately.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ipCountry(request: NextRequest): string | null;
|
package/routing/geo.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { REPLO_COUNTRY_OVERRIDE_COOKIE } from "../_vendor/schemas/routing/locale.mjs";
|
|
2
|
+
const COUNTRY_CODE = /^[A-Za-z]{2}$/;
|
|
3
|
+
function ipCountry(request) {
|
|
4
|
+
const override = request.cookies.get(REPLO_COUNTRY_OVERRIDE_COOKIE)?.value.trim();
|
|
5
|
+
if (override && COUNTRY_CODE.test(override)) {
|
|
6
|
+
return override.toUpperCase();
|
|
7
|
+
}
|
|
8
|
+
const header = request.headers.get("cf-ipcountry")?.trim().toUpperCase();
|
|
9
|
+
if (!header || !COUNTRY_CODE.test(header) || header === "XX") {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return header;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
ipCountry
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=geo.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../routing/geo.ts"],
|
|
4
|
+
"sourcesContent": ["import type { NextRequest } from \"next/server\";\n\nimport { REPLO_COUNTRY_OVERRIDE_COOKIE } from \"schemas/routing/locale\";\n\n// Cloudflare's header uses `XX` for \"unknown\" (`T1` for Tor fails the\n// letters-only check on its own).\nconst COUNTRY_CODE = /^[A-Za-z]{2}$/;\n\n/**\n * The visitor's ISO 3166-1 alpha-2 country, or null when unknown. The\n * override cookie wins so geo behavior is testable without a VPN \u2014 in dev\n * sandboxes, where the Cloudflare header never exists, and on the published\n * site alike. At worst the override lets a visitor pick which locale they\n * see, which the locale cookie already allows deliberately.\n */\nexport function ipCountry(request: NextRequest): string | null {\n const override = request.cookies\n .get(REPLO_COUNTRY_OVERRIDE_COOKIE)\n ?.value.trim();\n if (override && COUNTRY_CODE.test(override)) {\n return override.toUpperCase();\n }\n const header = request.headers.get(\"cf-ipcountry\")?.trim().toUpperCase();\n if (!header || !COUNTRY_CODE.test(header) || header === \"XX\") {\n return null;\n }\n return header;\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,qCAAqC;AAI9C,MAAM,eAAe;AASd,SAAS,UAAU,SAAqC;AAC7D,QAAM,WAAW,QAAQ,QACtB,IAAI,6BAA6B,GAChC,MAAM,KAAK;AACf,MAAI,YAAY,aAAa,KAAK,QAAQ,GAAG;AAC3C,WAAO,SAAS,YAAY;AAAA,EAC9B;AACA,QAAM,SAAS,QAAQ,QAAQ,IAAI,cAAc,GAAG,KAAK,EAAE,YAAY;AACvE,MAAI,CAAC,UAAU,CAAC,aAAa,KAAK,MAAM,KAAK,WAAW,MAAM;AAC5D,WAAO;AAAA,EACT;AACA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-facing entrypoint for locale routing authoring and helpers.
|
|
3
|
+
*
|
|
4
|
+
* The schema, negotiation, and URL helpers live in `schemas/routing` so the
|
|
5
|
+
* Website Builder can import them without depending on a published package,
|
|
6
|
+
* whose release cycle shouldn't gate editor-only changes. Sites can't import
|
|
7
|
+
* `schemas` directly — it is workspace-only and reaches generated sites
|
|
8
|
+
* through the SDK's vendoring step — so `middleware.ts` and language
|
|
9
|
+
* switchers import from here.
|
|
10
|
+
*/
|
|
11
|
+
export type { LocaleDetection, LocaleRoutingConfig, LocaleRoutingConfigInput, LocaleUrlMode, } from "../_vendor/schemas/routing/locale";
|
|
12
|
+
export { REPLO_COUNTRY_OVERRIDE_COOKIE, REPLO_LOCALE_COOKIE, reploLocaleRouting, } from "../_vendor/schemas/routing/locale";
|
|
13
|
+
export { localeUrl } from "../_vendor/schemas/routing/localeNegotiation";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REPLO_COUNTRY_OVERRIDE_COOKIE,
|
|
3
|
+
REPLO_LOCALE_COOKIE,
|
|
4
|
+
reploLocaleRouting
|
|
5
|
+
} from "../_vendor/schemas/routing/locale.mjs";
|
|
6
|
+
import { localeUrl } from "../_vendor/schemas/routing/localeNegotiation.mjs";
|
|
7
|
+
export {
|
|
8
|
+
REPLO_COUNTRY_OVERRIDE_COOKIE,
|
|
9
|
+
REPLO_LOCALE_COOKIE,
|
|
10
|
+
localeUrl,
|
|
11
|
+
reploLocaleRouting
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=locale.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../routing/locale.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Site-facing entrypoint for locale routing authoring and helpers.\n *\n * The schema, negotiation, and URL helpers live in `schemas/routing` so the\n * Website Builder can import them without depending on a published package,\n * whose release cycle shouldn't gate editor-only changes. Sites can't import\n * `schemas` directly \u2014 it is workspace-only and reaches generated sites\n * through the SDK's vendoring step \u2014 so `middleware.ts` and language\n * switchers import from here.\n */\nexport type {\n LocaleDetection,\n LocaleRoutingConfig,\n LocaleRoutingConfigInput,\n LocaleUrlMode,\n} from \"schemas/routing/locale\"; /* eslint-disable-line barrel-files/avoid-barrel-files, replo/no-export-from -- site-facing entrypoint intentionally re-exports the authoring API. */\nexport {\n REPLO_COUNTRY_OVERRIDE_COOKIE,\n REPLO_LOCALE_COOKIE,\n reploLocaleRouting,\n} from \"schemas/routing/locale\"; /* eslint-disable-line replo/no-export-from -- site-facing entrypoint intentionally re-exports the authoring API. */\nexport { localeUrl } from \"schemas/routing/localeNegotiation\"; /* eslint-disable-line replo/no-export-from -- site-facing entrypoint intentionally re-exports the authoring API. */\n"],
|
|
5
|
+
"mappings": "AAgBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|