@lovalingo/lovalingo 0.5.29 → 0.6.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 +34 -0
- package/dist/chunk-2FZR2AKF.mjs +88 -0
- package/dist/chunk-7D5LBV45.mjs +46 -0
- package/dist/chunk-CJOSN7RA.mjs +90 -0
- package/dist/chunk-VAHA2TOX.mjs +3440 -0
- package/dist/chunk-ZMRCSUM7.mjs +26 -0
- package/dist/chunk-ZVYKEEUF.mjs +220 -0
- package/dist/core.d.mts +131 -0
- package/dist/core.d.ts +131 -0
- package/dist/core.js +3561 -0
- package/dist/core.mjs +19 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -25
- package/dist/index.js +3885 -28
- package/dist/index.mjs +33 -0
- package/dist/react-router.d.mts +101 -0
- package/dist/react-router.d.ts +101 -0
- package/dist/react-router.js +353 -0
- package/dist/react-router.mjs +14 -0
- package/dist/tanstack-router.d.mts +22 -0
- package/dist/tanstack-router.d.ts +22 -0
- package/dist/tanstack-router.js +162 -0
- package/dist/tanstack-router.mjs +8 -0
- package/package.json +34 -3
- package/dist/__tests__/languageFlags.test.d.ts +0 -1
- package/dist/__tests__/languageFlags.test.js +0 -42
- package/dist/__tests__/mergeEntitlements.test.d.ts +0 -1
- package/dist/__tests__/mergeEntitlements.test.js +0 -27
- package/dist/components/AixsterProvider.d.ts +0 -1
- package/dist/components/AixsterProvider.js +0 -1
- package/dist/components/LangLink.d.ts +0 -20
- package/dist/components/LangLink.js +0 -38
- package/dist/components/LangRouter.d.ts +0 -37
- package/dist/components/LangRouter.js +0 -191
- package/dist/components/LanguageSwitcher.d.ts +0 -17
- package/dist/components/LanguageSwitcher.js +0 -257
- package/dist/components/LovalingoProvider.d.ts +0 -10
- package/dist/components/LovalingoProvider.js +0 -413
- package/dist/components/NavigationOverlay.d.ts +0 -6
- package/dist/components/NavigationOverlay.js +0 -22
- package/dist/components/provider/__tests__/seoUtils.test.d.ts +0 -1
- package/dist/components/provider/__tests__/seoUtils.test.js +0 -13
- package/dist/components/provider/editModeUtils.d.ts +0 -6
- package/dist/components/provider/editModeUtils.js +0 -59
- package/dist/components/provider/localeUtils.d.ts +0 -8
- package/dist/components/provider/localeUtils.js +0 -46
- package/dist/components/provider/providerConstants.d.ts +0 -12
- package/dist/components/provider/providerConstants.js +0 -11
- package/dist/components/provider/seoUtils.d.ts +0 -8
- package/dist/components/provider/seoUtils.js +0 -118
- package/dist/components/provider/useEditModeOverlay.d.ts +0 -7
- package/dist/components/provider/useEditModeOverlay.js +0 -134
- package/dist/components/provider/useHistoryNavigationPatch.d.ts +0 -3
- package/dist/components/provider/useHistoryNavigationPatch.js +0 -47
- package/dist/components/provider/useProviderCache.d.ts +0 -12
- package/dist/components/provider/useProviderCache.js +0 -82
- package/dist/context/AixsterContext.d.ts +0 -3
- package/dist/context/AixsterContext.js +0 -2
- package/dist/context/LangContext.d.ts +0 -1
- package/dist/context/LangContext.js +0 -2
- package/dist/context/LangRoutingContext.d.ts +0 -8
- package/dist/context/LangRoutingContext.js +0 -7
- package/dist/context/LovalingoContext.d.ts +0 -1
- package/dist/context/LovalingoContext.js +0 -1
- package/dist/hooks/provider/useBundleLoading.d.ts +0 -33
- package/dist/hooks/provider/useBundleLoading.js +0 -380
- package/dist/hooks/provider/useDomRules.d.ts +0 -15
- package/dist/hooks/provider/useDomRules.js +0 -38
- package/dist/hooks/provider/useLinkAutoPrefix.d.ts +0 -12
- package/dist/hooks/provider/useLinkAutoPrefix.js +0 -146
- package/dist/hooks/provider/useNavigationPrefetch.d.ts +0 -12
- package/dist/hooks/provider/useNavigationPrefetch.js +0 -82
- package/dist/hooks/provider/usePageviewTracking.d.ts +0 -10
- package/dist/hooks/provider/usePageviewTracking.js +0 -44
- package/dist/hooks/provider/usePrehide.d.ts +0 -5
- package/dist/hooks/provider/usePrehide.js +0 -72
- package/dist/hooks/provider/useSitemapLinkTag.d.ts +0 -7
- package/dist/hooks/provider/useSitemapLinkTag.js +0 -28
- package/dist/hooks/provider/useStringMissReporting.d.ts +0 -14
- package/dist/hooks/provider/useStringMissReporting.js +0 -155
- package/dist/hooks/useAixster.d.ts +0 -6
- package/dist/hooks/useAixster.js +0 -14
- package/dist/hooks/useAixsterEdit.d.ts +0 -5
- package/dist/hooks/useAixsterEdit.js +0 -13
- package/dist/hooks/useAixsterTranslate.d.ts +0 -4
- package/dist/hooks/useAixsterTranslate.js +0 -12
- package/dist/hooks/useLang.d.ts +0 -16
- package/dist/hooks/useLang.js +0 -23
- package/dist/hooks/useLangNavigate.d.ts +0 -24
- package/dist/hooks/useLangNavigate.js +0 -40
- package/dist/hooks/useLovalingo.d.ts +0 -1
- package/dist/hooks/useLovalingo.js +0 -1
- package/dist/hooks/useLovalingoEdit.d.ts +0 -1
- package/dist/hooks/useLovalingoEdit.js +0 -1
- package/dist/hooks/useLovalingoTranslate.d.ts +0 -1
- package/dist/hooks/useLovalingoTranslate.js +0 -1
- package/dist/types.d.ts +0 -76
- package/dist/types.js +0 -1
- package/dist/utils/api.d.ts +0 -42
- package/dist/utils/api.js +0 -395
- package/dist/utils/apiTypes.d.ts +0 -78
- package/dist/utils/apiTypes.js +0 -1
- package/dist/utils/apiUtils.d.ts +0 -4
- package/dist/utils/apiUtils.js +0 -54
- package/dist/utils/domRules.d.ts +0 -2
- package/dist/utils/domRules.js +0 -150
- package/dist/utils/hash.d.ts +0 -9
- package/dist/utils/hash.js +0 -27
- package/dist/utils/languageFlags.d.ts +0 -7
- package/dist/utils/languageFlags.js +0 -90
- package/dist/utils/logger.d.ts +0 -3
- package/dist/utils/logger.js +0 -40
- package/dist/utils/markerEngine.d.ts +0 -12
- package/dist/utils/markerEngine.js +0 -109
- package/dist/utils/markerEngineApply.d.ts +0 -3
- package/dist/utils/markerEngineApply.js +0 -136
- package/dist/utils/markerEngineConstants.d.ts +0 -10
- package/dist/utils/markerEngineConstants.js +0 -12
- package/dist/utils/markerEngineCritical.d.ts +0 -2
- package/dist/utils/markerEngineCritical.js +0 -98
- package/dist/utils/markerEngineDomUtils.d.ts +0 -8
- package/dist/utils/markerEngineDomUtils.js +0 -74
- package/dist/utils/markerEngineFilters.d.ts +0 -2
- package/dist/utils/markerEngineFilters.js +0 -26
- package/dist/utils/markerEngineMisses.d.ts +0 -5
- package/dist/utils/markerEngineMisses.js +0 -81
- package/dist/utils/markerEngineOriginals.d.ts +0 -5
- package/dist/utils/markerEngineOriginals.js +0 -29
- package/dist/utils/markerEngineScan.d.ts +0 -5
- package/dist/utils/markerEngineScan.js +0 -162
- package/dist/utils/markerEngineState.d.ts +0 -4
- package/dist/utils/markerEngineState.js +0 -14
- package/dist/utils/markerEngineStats.d.ts +0 -3
- package/dist/utils/markerEngineStats.js +0 -28
- package/dist/utils/markerEngineTranslations.d.ts +0 -3
- package/dist/utils/markerEngineTranslations.js +0 -49
- package/dist/utils/markerEngineTypes.d.ts +0 -62
- package/dist/utils/markerEngineTypes.js +0 -1
- package/dist/utils/markerEngineViewport.d.ts +0 -2
- package/dist/utils/markerEngineViewport.js +0 -27
- package/dist/utils/mergeEntitlements.d.ts +0 -2
- package/dist/utils/mergeEntitlements.js +0 -7
- package/dist/utils/nonLocalizedPaths.d.ts +0 -12
- package/dist/utils/nonLocalizedPaths.js +0 -136
- package/dist/utils/pathNormalizer.d.ts +0 -49
- package/dist/utils/pathNormalizer.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/tanstack-router.ts
|
|
21
|
+
var tanstack_router_exports = {};
|
|
22
|
+
__export(tanstack_router_exports, {
|
|
23
|
+
createLovalingoTanStackRewrite: () => createLovalingoTanStackRewrite
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(tanstack_router_exports);
|
|
26
|
+
|
|
27
|
+
// src/components/provider/providerConstants.ts
|
|
28
|
+
var LOCALE_STORAGE_KEY = "Lovalingo_locale";
|
|
29
|
+
|
|
30
|
+
// src/utils/nonLocalizedPaths.ts
|
|
31
|
+
var GLOBAL_NON_LOCALIZED_APP_PATHS = /* @__PURE__ */ new Set(["/robots.txt", "/sitemap.xml"]);
|
|
32
|
+
function isGlobalNonLocalizedPath(pathname) {
|
|
33
|
+
const input = (pathname || "").toString();
|
|
34
|
+
if (!input.startsWith("/")) return false;
|
|
35
|
+
if (GLOBAL_NON_LOCALIZED_APP_PATHS.has(input)) return true;
|
|
36
|
+
if (input.startsWith("/.well-known/")) return true;
|
|
37
|
+
return /\.(?:png|jpg|jpeg|gif|svg|webp|avif|ico|css|js|map|json|xml|txt|pdf|zip|gz|br|woff2?|ttf|eot)$/i.test(input);
|
|
38
|
+
}
|
|
39
|
+
function matchesNonLocalizedRules(pathname, rules) {
|
|
40
|
+
const input = (pathname || "").toString();
|
|
41
|
+
if (!input.startsWith("/")) return false;
|
|
42
|
+
if (!Array.isArray(rules) || rules.length === 0) return false;
|
|
43
|
+
for (const rule of rules) {
|
|
44
|
+
const pattern = typeof rule?.pattern === "string" ? rule.pattern : "";
|
|
45
|
+
const matchType = rule?.match_type;
|
|
46
|
+
if (!pattern) continue;
|
|
47
|
+
if (matchType === "exact") {
|
|
48
|
+
if (input === pattern) return true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (matchType === "prefix") {
|
|
52
|
+
if (input === pattern) return true;
|
|
53
|
+
const normalizedPrefix = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
54
|
+
if (!normalizedPrefix || normalizedPrefix === "/") return true;
|
|
55
|
+
if (input.startsWith(`${normalizedPrefix}/`)) return true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (matchType === "regex") {
|
|
59
|
+
try {
|
|
60
|
+
if (new RegExp(pattern).test(input)) return true;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
function isNonLocalizedPath(pathname, rules) {
|
|
68
|
+
return isGlobalNonLocalizedPath(pathname) || matchesNonLocalizedRules(pathname, rules);
|
|
69
|
+
}
|
|
70
|
+
function stripLocalePrefix(pathname, locales) {
|
|
71
|
+
const input = (pathname || "").toString();
|
|
72
|
+
if (!input.startsWith("/")) return input;
|
|
73
|
+
const parts = input.split("/").filter(Boolean);
|
|
74
|
+
if (parts.length === 0) return "/";
|
|
75
|
+
const first = parts[0] || "";
|
|
76
|
+
if (!first || !Array.isArray(locales) || !locales.includes(first)) return input;
|
|
77
|
+
const rest = `/${parts.slice(1).join("/")}`;
|
|
78
|
+
return rest === "" ? "/" : rest;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/tanstack-router.ts
|
|
82
|
+
function normalizeLocale(value) {
|
|
83
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
84
|
+
}
|
|
85
|
+
function uniq(items) {
|
|
86
|
+
return Array.from(new Set(items));
|
|
87
|
+
}
|
|
88
|
+
function buildPathWithLocale(locale, pathname) {
|
|
89
|
+
const normalizedPath = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
90
|
+
if (normalizedPath === "/") return `/${locale}`;
|
|
91
|
+
return `/${locale}${normalizedPath}`;
|
|
92
|
+
}
|
|
93
|
+
function cloneUrlWithPathname(url, pathname) {
|
|
94
|
+
const next = new URL(url.href);
|
|
95
|
+
next.pathname = pathname || "/";
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
function readStoredLocale(supportedLocales) {
|
|
99
|
+
if (typeof window === "undefined") return "";
|
|
100
|
+
try {
|
|
101
|
+
const stored = normalizeLocale(window.localStorage?.getItem(LOCALE_STORAGE_KEY));
|
|
102
|
+
return supportedLocales.includes(stored) ? stored : "";
|
|
103
|
+
} catch {
|
|
104
|
+
return "";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function detectLocaleFromPath(pathname, supportedLocales) {
|
|
108
|
+
const first = pathname.split("/").filter(Boolean)[0] || "";
|
|
109
|
+
const normalized = normalizeLocale(first);
|
|
110
|
+
return supportedLocales.includes(normalized) ? normalized : "";
|
|
111
|
+
}
|
|
112
|
+
function createLovalingoTanStackRewrite({
|
|
113
|
+
defaultLocale,
|
|
114
|
+
locales,
|
|
115
|
+
prefixDefaultLocale = true,
|
|
116
|
+
nonLocalizedPaths = [],
|
|
117
|
+
getLocale
|
|
118
|
+
}) {
|
|
119
|
+
const normalizedDefaultLocale = normalizeLocale(defaultLocale);
|
|
120
|
+
const supportedLocales = uniq([normalizedDefaultLocale, ...(locales || []).map(normalizeLocale)].filter(Boolean));
|
|
121
|
+
const fallbackLocale = normalizedDefaultLocale || supportedLocales[0] || "en";
|
|
122
|
+
let activeLocale = fallbackLocale;
|
|
123
|
+
const resolveOutputLocale = (pathname) => {
|
|
124
|
+
const explicitLocale = normalizeLocale(getLocale?.());
|
|
125
|
+
if (supportedLocales.includes(explicitLocale)) {
|
|
126
|
+
activeLocale = explicitLocale;
|
|
127
|
+
return explicitLocale;
|
|
128
|
+
}
|
|
129
|
+
const pathLocale = detectLocaleFromPath(pathname, supportedLocales);
|
|
130
|
+
if (pathLocale) {
|
|
131
|
+
activeLocale = pathLocale;
|
|
132
|
+
return pathLocale;
|
|
133
|
+
}
|
|
134
|
+
const storedLocale = readStoredLocale(supportedLocales);
|
|
135
|
+
if (storedLocale) {
|
|
136
|
+
activeLocale = storedLocale;
|
|
137
|
+
return storedLocale;
|
|
138
|
+
}
|
|
139
|
+
return supportedLocales.includes(activeLocale) ? activeLocale : fallbackLocale;
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
input: ({ url }) => {
|
|
143
|
+
const pathLocale = detectLocaleFromPath(url.pathname, supportedLocales);
|
|
144
|
+
if (!pathLocale) return void 0;
|
|
145
|
+
activeLocale = pathLocale;
|
|
146
|
+
const stripped = stripLocalePrefix(url.pathname, supportedLocales);
|
|
147
|
+
return cloneUrlWithPathname(url, stripped || "/");
|
|
148
|
+
},
|
|
149
|
+
output: ({ url }) => {
|
|
150
|
+
const internalPath = stripLocalePrefix(url.pathname, supportedLocales);
|
|
151
|
+
if (isNonLocalizedPath(internalPath, nonLocalizedPaths)) return void 0;
|
|
152
|
+
if (detectLocaleFromPath(url.pathname, supportedLocales)) return void 0;
|
|
153
|
+
const locale = resolveOutputLocale(url.pathname);
|
|
154
|
+
if (!prefixDefaultLocale && locale === fallbackLocale) return void 0;
|
|
155
|
+
return cloneUrlWithPathname(url, buildPathWithLocale(locale, internalPath));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
160
|
+
0 && (module.exports = {
|
|
161
|
+
createLovalingoTanStackRewrite
|
|
162
|
+
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovalingo/lovalingo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "React translation runtime with i18n routing, deterministic bundles + DOM rules, and zero-flash rendering.",
|
|
5
|
-
"main": "dist/index.js",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./core": {
|
|
15
|
+
"types": "./dist/core.d.ts",
|
|
16
|
+
"import": "./dist/core.mjs",
|
|
17
|
+
"require": "./dist/core.js"
|
|
18
|
+
},
|
|
19
|
+
"./react-router": {
|
|
20
|
+
"types": "./dist/react-router.d.ts",
|
|
21
|
+
"import": "./dist/react-router.mjs",
|
|
22
|
+
"require": "./dist/react-router.js"
|
|
23
|
+
},
|
|
24
|
+
"./tanstack-router": {
|
|
25
|
+
"types": "./dist/tanstack-router.d.ts",
|
|
26
|
+
"import": "./dist/tanstack-router.mjs",
|
|
27
|
+
"require": "./dist/tanstack-router.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
7
31
|
"publishConfig": {
|
|
8
32
|
"access": "public"
|
|
9
33
|
},
|
|
@@ -11,7 +35,7 @@
|
|
|
11
35
|
"dist"
|
|
12
36
|
],
|
|
13
37
|
"scripts": {
|
|
14
|
-
"build": "
|
|
38
|
+
"build": "tsup src/index.tsx src/core.tsx src/react-router.tsx src/tanstack-router.ts --format esm,cjs --dts --external react,react-dom,react-router-dom --clean",
|
|
15
39
|
"prepublishOnly": "npm run build"
|
|
16
40
|
},
|
|
17
41
|
"keywords": [
|
|
@@ -36,10 +60,17 @@
|
|
|
36
60
|
"react-dom": "^18.0.0",
|
|
37
61
|
"react-router-dom": "^6.30.3"
|
|
38
62
|
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"react-router-dom": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
39
68
|
"devDependencies": {
|
|
40
69
|
"@types/node": "^24.9.1",
|
|
41
70
|
"@types/react": "^18.3.1",
|
|
42
71
|
"@types/react-dom": "^18.3.0",
|
|
72
|
+
"react-router-dom": "^6.30.4",
|
|
73
|
+
"tsup": "^8.5.1",
|
|
43
74
|
"typescript": "^5.0.0"
|
|
44
75
|
},
|
|
45
76
|
"repository": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { countryCodeToFlagEmoji, normalizeLocaleCode, parseLocale, resolveLocaleFlag } from "../utils/languageFlags";
|
|
3
|
-
describe("languageFlags", () => {
|
|
4
|
-
it("normalizes locale codes with case and underscore variants", () => {
|
|
5
|
-
expect(normalizeLocaleCode("TR")).toBe("tr");
|
|
6
|
-
expect(normalizeLocaleCode("tr_TR")).toBe("tr-tr");
|
|
7
|
-
expect(normalizeLocaleCode("tr-TR")).toBe("tr-tr");
|
|
8
|
-
});
|
|
9
|
-
it("parses language and region", () => {
|
|
10
|
-
expect(parseLocale("en-CA")).toEqual({ language: "en", region: "CA" });
|
|
11
|
-
expect(parseLocale("fr")).toEqual({ language: "fr", region: null });
|
|
12
|
-
});
|
|
13
|
-
it("returns consistent flag for TR variants", () => {
|
|
14
|
-
expect(resolveLocaleFlag("TR")).toBe("🇹🇷");
|
|
15
|
-
expect(resolveLocaleFlag("tr")).toBe("🇹🇷");
|
|
16
|
-
expect(resolveLocaleFlag("tr-TR")).toBe("🇹🇷");
|
|
17
|
-
expect(resolveLocaleFlag("tr_TR")).toBe("🇹🇷");
|
|
18
|
-
});
|
|
19
|
-
it("uses region flag when locale includes region", () => {
|
|
20
|
-
expect(resolveLocaleFlag("en-CA")).toBe("🇨🇦");
|
|
21
|
-
expect(resolveLocaleFlag("pt-BR")).toBe("🇧🇷");
|
|
22
|
-
});
|
|
23
|
-
it("uses language default region when locale has no region", () => {
|
|
24
|
-
expect(resolveLocaleFlag("en")).toBe("🇬🇧");
|
|
25
|
-
expect(resolveLocaleFlag("zh")).toBe("🇨🇳");
|
|
26
|
-
expect(resolveLocaleFlag("pt")).toBe("🇵🇹");
|
|
27
|
-
});
|
|
28
|
-
it("falls back to globe for unknown or invalid locale", () => {
|
|
29
|
-
expect(resolveLocaleFlag("zzz")).toBe("🌐");
|
|
30
|
-
expect(resolveLocaleFlag("")).toBe("🌐");
|
|
31
|
-
expect(resolveLocaleFlag(null)).toBe("🌐");
|
|
32
|
-
});
|
|
33
|
-
it("never falls back to white flag", () => {
|
|
34
|
-
expect(resolveLocaleFlag("zzz")).not.toBe("🏳️");
|
|
35
|
-
expect(resolveLocaleFlag("foo-bar-baz")).not.toBe("🏳️");
|
|
36
|
-
});
|
|
37
|
-
it("converts country code to emoji", () => {
|
|
38
|
-
expect(countryCodeToFlagEmoji("ca")).toBe("🇨🇦");
|
|
39
|
-
expect(countryCodeToFlagEmoji("PT")).toBe("🇵🇹");
|
|
40
|
-
expect(countryCodeToFlagEmoji("ZZZ")).toBeNull();
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { mergeEntitlementsSeoEnabled } from "../utils/mergeEntitlements";
|
|
3
|
-
describe("mergeEntitlementsSeoEnabled", () => {
|
|
4
|
-
it("merges seoEnabled when provided", () => {
|
|
5
|
-
const entitlements = {
|
|
6
|
-
tier: "starter",
|
|
7
|
-
maxTargetLocales: 1,
|
|
8
|
-
allowedTargetLocales: [],
|
|
9
|
-
brandingRequired: true,
|
|
10
|
-
hreflangEnabled: false,
|
|
11
|
-
};
|
|
12
|
-
const merged = mergeEntitlementsSeoEnabled(entitlements, false);
|
|
13
|
-
expect(merged?.seoEnabled).toBe(false);
|
|
14
|
-
});
|
|
15
|
-
it("returns the same object when seoEnabled is not a boolean", () => {
|
|
16
|
-
const entitlements = {
|
|
17
|
-
tier: "startup",
|
|
18
|
-
maxTargetLocales: 3,
|
|
19
|
-
allowedTargetLocales: ["zh"],
|
|
20
|
-
brandingRequired: false,
|
|
21
|
-
hreflangEnabled: true,
|
|
22
|
-
seoEnabled: true,
|
|
23
|
-
};
|
|
24
|
-
const merged = mergeEntitlementsSeoEnabled(entitlements, null);
|
|
25
|
-
expect(merged).toBe(entitlements);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { LovalingoProvider } from "./LovalingoProvider";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { LovalingoProvider } from "./LovalingoProvider";
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { LinkProps } from 'react-router-dom';
|
|
3
|
-
/**
|
|
4
|
-
* LangLink - Language-aware Link component
|
|
5
|
-
*
|
|
6
|
-
* Automatically prepends the current language to all navigation paths
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* ```tsx
|
|
10
|
-
* // Instead of:
|
|
11
|
-
* <Link to={`/${locale}/pricing`}>Pricing</Link>
|
|
12
|
-
*
|
|
13
|
-
* // Just write:
|
|
14
|
-
* <LangLink to="pricing">Pricing</LangLink>
|
|
15
|
-
* // Automatically becomes /en/pricing or /fr/pricing
|
|
16
|
-
* ```
|
|
17
|
-
*
|
|
18
|
-
* All props from React Router's <Link> are supported
|
|
19
|
-
*/
|
|
20
|
-
export declare function LangLink({ to, ...props }: LinkProps): React.JSX.Element;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Link } from 'react-router-dom';
|
|
3
|
-
import { useLang } from '../hooks/useLang';
|
|
4
|
-
import { useContext } from 'react';
|
|
5
|
-
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
6
|
-
import { isNonLocalizedPath } from '../utils/nonLocalizedPaths';
|
|
7
|
-
/**
|
|
8
|
-
* LangLink - Language-aware Link component
|
|
9
|
-
*
|
|
10
|
-
* Automatically prepends the current language to all navigation paths
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* ```tsx
|
|
14
|
-
* // Instead of:
|
|
15
|
-
* <Link to={`/${locale}/pricing`}>Pricing</Link>
|
|
16
|
-
*
|
|
17
|
-
* // Just write:
|
|
18
|
-
* <LangLink to="pricing">Pricing</LangLink>
|
|
19
|
-
* // Automatically becomes /en/pricing or /fr/pricing
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* All props from React Router's <Link> are supported
|
|
23
|
-
*/
|
|
24
|
-
export function LangLink({ to, ...props }) {
|
|
25
|
-
const lang = useLang();
|
|
26
|
-
const routing = useContext(LangRoutingContext);
|
|
27
|
-
// If 'to' is a string, prepend language
|
|
28
|
-
const langTo = typeof to === 'string'
|
|
29
|
-
? (() => {
|
|
30
|
-
const trimmed = (to || '').toString().trim();
|
|
31
|
-
const normalized = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
32
|
-
return isNonLocalizedPath(normalized, routing.nonLocalizedPaths)
|
|
33
|
-
? normalized
|
|
34
|
-
: `/${lang}${normalized}`;
|
|
35
|
-
})()
|
|
36
|
-
: to;
|
|
37
|
-
return React.createElement(Link, { ...props, to: langTo });
|
|
38
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
interface LangRouterProps {
|
|
3
|
-
children: React.ReactNode;
|
|
4
|
-
defaultLang: string;
|
|
5
|
-
langs: string[];
|
|
6
|
-
navigateRef?: React.MutableRefObject<((path: string) => void) | undefined>;
|
|
7
|
-
apiBase?: string;
|
|
8
|
-
apiKey?: string;
|
|
9
|
-
publicAnonKey?: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* LangRouter - Drop-in replacement for BrowserRouter that automatically handles language routing
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* ```tsx
|
|
16
|
-
* const navigateRef = useRef();
|
|
17
|
-
*
|
|
18
|
-
* <LangRouter defaultLang="en" langs={['en', 'fr', 'de', 'ko']} navigateRef={navigateRef}>
|
|
19
|
-
* <Routes>
|
|
20
|
-
* <Route path="/" element={<Home />} />
|
|
21
|
-
* <Route path="home" element={<Home />} />
|
|
22
|
-
* <Route path="pricing" element={<Pricing />} />
|
|
23
|
-
* </Routes>
|
|
24
|
-
* </LangRouter>
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* This automatically creates routes:
|
|
28
|
-
* - /en (root)
|
|
29
|
-
* - /en/home
|
|
30
|
-
* - /en/pricing
|
|
31
|
-
* - /fr (root)
|
|
32
|
-
* - /fr/home
|
|
33
|
-
* - /fr/pricing
|
|
34
|
-
* - etc.
|
|
35
|
-
*/
|
|
36
|
-
export declare function LangRouter({ children, defaultLang, langs, navigateRef, apiBase, apiKey, publicAnonKey }: LangRouterProps): React.JSX.Element;
|
|
37
|
-
export {};
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
-
import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
3
|
-
import { LangContext } from '../context/LangContext';
|
|
4
|
-
import { LangRoutingContext } from '../context/LangRoutingContext';
|
|
5
|
-
import { isNonLocalizedPath, parseBootstrapInactivePages, parseBootstrapNonLocalizedPaths } from '../utils/nonLocalizedPaths';
|
|
6
|
-
import { logDebug } from '../utils/logger';
|
|
7
|
-
/**
|
|
8
|
-
* NavigateExporter - Internal component that exports navigate function via ref
|
|
9
|
-
*/
|
|
10
|
-
function NavigateExporter({ navigateRef }) {
|
|
11
|
-
const navigate = useNavigate();
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
if (navigateRef) {
|
|
14
|
-
navigateRef.current = navigate;
|
|
15
|
-
}
|
|
16
|
-
}, [navigate, navigateRef]);
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* LangGuard - Internal component that validates language and provides it to children
|
|
21
|
-
*/
|
|
22
|
-
function LangGuard({ lang, nonLocalizedPaths, defaultLang, }) {
|
|
23
|
-
const location = useLocation();
|
|
24
|
-
// If the URL is language-prefixed but the underlying route is non-localized (e.g. robots/sitemap),
|
|
25
|
-
// redirect to the canonical non-localized path.
|
|
26
|
-
const prefix = `/${lang}`;
|
|
27
|
-
const restPath = location.pathname.startsWith(prefix) ? location.pathname.slice(prefix.length) || '/' : location.pathname;
|
|
28
|
-
// Why: only explicit non-localized rules may strip the locale prefix; inactive pages must never affect client routing.
|
|
29
|
-
if (isNonLocalizedPath(restPath, nonLocalizedPaths)) {
|
|
30
|
-
const nextPath = `${restPath}${location.search}${location.hash}`;
|
|
31
|
-
return React.createElement(Navigate, { to: nextPath, replace: true });
|
|
32
|
-
}
|
|
33
|
-
// Valid language - render children (user's routes)
|
|
34
|
-
return (React.createElement(LangContext.Provider, { value: lang },
|
|
35
|
-
React.createElement(Outlet, { context: { lang } })));
|
|
36
|
-
}
|
|
37
|
-
function RedirectToDefaultLang({ defaultLang, children, nonLocalizedPaths, routingStatus, }) {
|
|
38
|
-
const location = useLocation();
|
|
39
|
-
const navigate = useNavigate();
|
|
40
|
-
const shouldSkip = isNonLocalizedPath(location.pathname, nonLocalizedPaths);
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
if (shouldSkip)
|
|
43
|
-
return;
|
|
44
|
-
if (routingStatus === "loading")
|
|
45
|
-
return;
|
|
46
|
-
const nextPath = location.pathname === "/" || location.pathname === ""
|
|
47
|
-
? `/${defaultLang}${location.search}${location.hash}`
|
|
48
|
-
: `/${defaultLang}${location.pathname}${location.search}${location.hash}`;
|
|
49
|
-
const current = `${location.pathname}${location.search}${location.hash}`;
|
|
50
|
-
if (nextPath === current)
|
|
51
|
-
return;
|
|
52
|
-
navigate(nextPath, { replace: true });
|
|
53
|
-
}, [defaultLang, location.hash, location.pathname, location.search, navigate, routingStatus, shouldSkip]);
|
|
54
|
-
return React.createElement(React.Fragment, null, children);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* LangRouter - Drop-in replacement for BrowserRouter that automatically handles language routing
|
|
58
|
-
*
|
|
59
|
-
* Usage:
|
|
60
|
-
* ```tsx
|
|
61
|
-
* const navigateRef = useRef();
|
|
62
|
-
*
|
|
63
|
-
* <LangRouter defaultLang="en" langs={['en', 'fr', 'de', 'ko']} navigateRef={navigateRef}>
|
|
64
|
-
* <Routes>
|
|
65
|
-
* <Route path="/" element={<Home />} />
|
|
66
|
-
* <Route path="home" element={<Home />} />
|
|
67
|
-
* <Route path="pricing" element={<Pricing />} />
|
|
68
|
-
* </Routes>
|
|
69
|
-
* </LangRouter>
|
|
70
|
-
* ```
|
|
71
|
-
*
|
|
72
|
-
* This automatically creates routes:
|
|
73
|
-
* - /en (root)
|
|
74
|
-
* - /en/home
|
|
75
|
-
* - /en/pricing
|
|
76
|
-
* - /fr (root)
|
|
77
|
-
* - /fr/home
|
|
78
|
-
* - /fr/pricing
|
|
79
|
-
* - etc.
|
|
80
|
-
*/
|
|
81
|
-
export function LangRouter({ children, defaultLang, langs, navigateRef, apiBase, apiKey, publicAnonKey }) {
|
|
82
|
-
const metaKey = typeof document !== "undefined"
|
|
83
|
-
? document.querySelector('meta[name="lovalingo-public-anon-key"]')?.content?.trim() || ""
|
|
84
|
-
: "";
|
|
85
|
-
const globals = globalThis;
|
|
86
|
-
const resolvedApiKey = (typeof apiKey === "string" && apiKey.trim().length > 0
|
|
87
|
-
? apiKey
|
|
88
|
-
: typeof publicAnonKey === "string" && publicAnonKey.trim().length > 0
|
|
89
|
-
? publicAnonKey
|
|
90
|
-
: globals.__LOVALINGO_PUBLIC_ANON_KEY__ || globals.__LOVALINGO_API_KEY__ || metaKey || "").trim();
|
|
91
|
-
const resolvedApiBase = (typeof apiBase === "string" && apiBase.trim().length > 0
|
|
92
|
-
? apiBase.trim()
|
|
93
|
-
: "https://cdn.lovalingo.com");
|
|
94
|
-
const nonLocalizedStorageKey = useMemo(() => `Lovalingo_non_localized_paths:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
|
|
95
|
-
const inactivePagesStorageKey = useMemo(() => `Lovalingo_inactive_pages:${resolvedApiKey || "anonymous"}`, [resolvedApiKey]);
|
|
96
|
-
const [nonLocalizedPaths, setNonLocalizedPaths] = useState(() => {
|
|
97
|
-
if (typeof window === "undefined")
|
|
98
|
-
return [];
|
|
99
|
-
if (!resolvedApiKey)
|
|
100
|
-
return [];
|
|
101
|
-
try {
|
|
102
|
-
const raw = localStorage.getItem(nonLocalizedStorageKey);
|
|
103
|
-
if (!raw)
|
|
104
|
-
return [];
|
|
105
|
-
const parsed = JSON.parse(raw);
|
|
106
|
-
return parseBootstrapNonLocalizedPaths(parsed);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
const [inactivePages, setInactivePages] = useState(() => {
|
|
113
|
-
if (typeof window === "undefined")
|
|
114
|
-
return [];
|
|
115
|
-
if (!resolvedApiKey)
|
|
116
|
-
return [];
|
|
117
|
-
try {
|
|
118
|
-
const raw = localStorage.getItem(inactivePagesStorageKey);
|
|
119
|
-
if (!raw)
|
|
120
|
-
return [];
|
|
121
|
-
const parsed = JSON.parse(raw);
|
|
122
|
-
return parseBootstrapInactivePages(parsed);
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
const [routingStatus, setRoutingStatus] = useState(() => {
|
|
129
|
-
if (!resolvedApiKey)
|
|
130
|
-
return "unknown";
|
|
131
|
-
return nonLocalizedPaths.length > 0 || inactivePages.length > 0 ? "ready" : "loading";
|
|
132
|
-
});
|
|
133
|
-
const fetchRoutingConfig = useCallback(async () => {
|
|
134
|
-
if (typeof window === "undefined")
|
|
135
|
-
return;
|
|
136
|
-
if (!resolvedApiKey)
|
|
137
|
-
return;
|
|
138
|
-
const pathParam = window.location.pathname + window.location.search;
|
|
139
|
-
const requestUrl = `${resolvedApiBase}/functions/v1/bootstrap?key=${encodeURIComponent(resolvedApiKey)}&locale=${encodeURIComponent(defaultLang)}&path=${encodeURIComponent(pathParam)}`;
|
|
140
|
-
const response = await fetch(requestUrl);
|
|
141
|
-
const resolvedResponse = response.status === 304 ? await fetch(requestUrl, { cache: "force-cache" }) : response;
|
|
142
|
-
if (!resolvedResponse.ok)
|
|
143
|
-
throw new Error(`bootstrap HTTP ${resolvedResponse.status}`);
|
|
144
|
-
const data = (await resolvedResponse.json());
|
|
145
|
-
const record = (data || {});
|
|
146
|
-
return {
|
|
147
|
-
nonLocalizedPaths: parseBootstrapNonLocalizedPaths(record["non_localized_paths"]),
|
|
148
|
-
inactivePages: parseBootstrapInactivePages(record["inactive_pages"]),
|
|
149
|
-
};
|
|
150
|
-
}, [defaultLang, resolvedApiBase, resolvedApiKey]);
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
let cancelled = false;
|
|
153
|
-
void (async () => {
|
|
154
|
-
if (!resolvedApiKey)
|
|
155
|
-
return;
|
|
156
|
-
setRoutingStatus((prev) => (prev === "ready" ? prev : "loading"));
|
|
157
|
-
try {
|
|
158
|
-
const next = await fetchRoutingConfig();
|
|
159
|
-
if (cancelled || !next)
|
|
160
|
-
return;
|
|
161
|
-
setNonLocalizedPaths(next.nonLocalizedPaths);
|
|
162
|
-
setInactivePages(next.inactivePages);
|
|
163
|
-
setRoutingStatus("ready");
|
|
164
|
-
try {
|
|
165
|
-
localStorage.setItem(nonLocalizedStorageKey, JSON.stringify(next.nonLocalizedPaths));
|
|
166
|
-
localStorage.setItem(inactivePagesStorageKey, JSON.stringify(next.inactivePages));
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
// ignore
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
if (cancelled)
|
|
174
|
-
return;
|
|
175
|
-
setRoutingStatus("error");
|
|
176
|
-
logDebug("[Lovalingo] Failed to fetch routing config:", err);
|
|
177
|
-
}
|
|
178
|
-
})();
|
|
179
|
-
return () => {
|
|
180
|
-
cancelled = true;
|
|
181
|
-
};
|
|
182
|
-
}, [fetchRoutingConfig, inactivePagesStorageKey, nonLocalizedStorageKey, resolvedApiKey]);
|
|
183
|
-
return (React.createElement(BrowserRouter, null,
|
|
184
|
-
React.createElement(NavigateExporter, { navigateRef: navigateRef }),
|
|
185
|
-
React.createElement(LangRoutingContext.Provider, { value: { defaultLang, nonLocalizedPaths, inactivePages, status: routingStatus } },
|
|
186
|
-
React.createElement(Routes, null,
|
|
187
|
-
langs.map((lang) => (React.createElement(Route, { key: lang, path: `${lang}/*`, element: React.createElement(LangGuard, { lang: lang, nonLocalizedPaths: nonLocalizedPaths, defaultLang: defaultLang }) },
|
|
188
|
-
React.createElement(Route, { index: true, element: React.createElement(React.Fragment, null, children) }),
|
|
189
|
-
React.createElement(Route, { path: "*", element: React.createElement(React.Fragment, null, children) })))),
|
|
190
|
-
React.createElement(Route, { path: "*", element: React.createElement(RedirectToDefaultLang, { defaultLang: defaultLang, nonLocalizedPaths: nonLocalizedPaths, routingStatus: routingStatus }, children) })))));
|
|
191
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
interface LanguageSwitcherProps {
|
|
3
|
-
locales: string[];
|
|
4
|
-
currentLocale: string;
|
|
5
|
-
onLocaleChange: (locale: string) => void;
|
|
6
|
-
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
7
|
-
offsetY?: number;
|
|
8
|
-
theme?: 'dark' | 'light';
|
|
9
|
-
branding?: {
|
|
10
|
-
required?: boolean;
|
|
11
|
-
enabled?: boolean;
|
|
12
|
-
label?: string;
|
|
13
|
-
href?: string;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
export declare const LanguageSwitcher: React.FC<LanguageSwitcherProps>;
|
|
17
|
-
export {};
|