@pure-ds/core 0.7.32 → 0.7.33
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/dist/types/src/js/pds-core/pds-config.d.ts.map +1 -1
- package/package.json +3 -1
- package/public/assets/js/app.js +1 -1
- package/public/assets/js/pds-manager.js +12 -12
- package/public/assets/pds/core/pds-manager.js +12 -12
- package/src/js/common/ask.js +530 -0
- package/src/js/common/common.js +122 -0
- package/src/js/common/font-loader.js +202 -0
- package/src/js/common/localization-resource-provider.js +274 -0
- package/src/js/common/localization.js +839 -0
- package/src/js/common/msg.js +9 -0
- package/src/js/common/pds-core/pds.d.ts +128 -0
- package/src/js/common/pds-log.js +144 -0
- package/src/js/common/toast.js +122 -0
- package/src/js/pds-core/pds-config.js +20 -2
- package/src/js/pds-singleton.js +49 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font Loading Utility
|
|
3
|
+
* Automatically loads fonts from Google Fonts when they're not available in the browser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PDS } from "../pds-singleton.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if a font is available in the browser
|
|
10
|
+
* @param {string} fontName - The name of the font to check
|
|
11
|
+
* @returns {boolean} True if the font is available
|
|
12
|
+
*/
|
|
13
|
+
function isFontAvailable(fontName) {
|
|
14
|
+
// Clean up font name (remove quotes and extra spacing)
|
|
15
|
+
const cleanName = fontName.replace(/['"]/g, '').trim();
|
|
16
|
+
|
|
17
|
+
// System fonts that are always available
|
|
18
|
+
const systemFonts = [
|
|
19
|
+
'system-ui',
|
|
20
|
+
'-apple-system',
|
|
21
|
+
'sans-serif',
|
|
22
|
+
'serif',
|
|
23
|
+
'monospace',
|
|
24
|
+
'cursive',
|
|
25
|
+
'fantasy',
|
|
26
|
+
'ui-sans-serif',
|
|
27
|
+
'ui-serif',
|
|
28
|
+
'ui-monospace',
|
|
29
|
+
'ui-rounded'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
if (systemFonts.includes(cleanName.toLowerCase())) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Use canvas-based detection
|
|
37
|
+
const canvas = document.createElement('canvas');
|
|
38
|
+
const context = canvas.getContext('2d');
|
|
39
|
+
|
|
40
|
+
if (!context) return false;
|
|
41
|
+
|
|
42
|
+
const testString = 'mmmmmmmmmmlli'; // Characters with varying widths
|
|
43
|
+
const testSize = '72px';
|
|
44
|
+
const baselineFont = 'monospace';
|
|
45
|
+
|
|
46
|
+
// Measure with baseline font
|
|
47
|
+
context.font = `${testSize} ${baselineFont}`;
|
|
48
|
+
const baselineWidth = context.measureText(testString).width;
|
|
49
|
+
|
|
50
|
+
// Measure with test font
|
|
51
|
+
context.font = `${testSize} "${cleanName}", ${baselineFont}`;
|
|
52
|
+
const testWidth = context.measureText(testString).width;
|
|
53
|
+
|
|
54
|
+
return baselineWidth !== testWidth;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extracts the primary font name from a font-family string
|
|
59
|
+
* @param {string} fontFamily - Font family string (e.g., "Roboto, sans-serif")
|
|
60
|
+
* @returns {string} The primary font name
|
|
61
|
+
*/
|
|
62
|
+
function extractPrimaryFont(fontFamily) {
|
|
63
|
+
if (!fontFamily) return null;
|
|
64
|
+
|
|
65
|
+
// Split by comma and get first font
|
|
66
|
+
const fonts = fontFamily.split(',').map(f => f.trim());
|
|
67
|
+
const primaryFont = fonts[0];
|
|
68
|
+
|
|
69
|
+
// Remove quotes
|
|
70
|
+
return primaryFont.replace(/['"]/g, '').trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Loads a Google Font dynamically
|
|
75
|
+
* @param {string} fontFamily - The font family to load (can be comma-separated list)
|
|
76
|
+
* @param {Object} options - Loading options
|
|
77
|
+
* @param {number[]} options.weights - Font weights to load (default: [400, 500, 600, 700])
|
|
78
|
+
* @param {boolean} options.italic - Whether to include italic variants (default: false)
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
export async function loadGoogleFont(fontFamily, options = {}) {
|
|
82
|
+
if (!fontFamily) {
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const {
|
|
87
|
+
weights = [400, 500, 600, 700],
|
|
88
|
+
italic = false
|
|
89
|
+
} = options;
|
|
90
|
+
|
|
91
|
+
const primaryFont = extractPrimaryFont(fontFamily);
|
|
92
|
+
|
|
93
|
+
if (!primaryFont) {
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if font is already available
|
|
98
|
+
if (isFontAvailable(primaryFont)) {
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if font link already exists
|
|
103
|
+
const encodedFont = encodeURIComponent(primaryFont);
|
|
104
|
+
const existingLink = document.querySelector(
|
|
105
|
+
`link[href*="fonts.googleapis.com"][href*="${encodedFont}"]`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (existingLink) {
|
|
109
|
+
PDS.log("log", `Font "${primaryFont}" is already loading or loaded`);
|
|
110
|
+
return Promise.resolve();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
PDS.log("log", `Loading font "${primaryFont}" from Google Fonts...`);
|
|
114
|
+
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const link = document.createElement('link');
|
|
117
|
+
link.rel = 'stylesheet';
|
|
118
|
+
|
|
119
|
+
// Build Google Fonts URL with specified weights
|
|
120
|
+
const weightsParam = italic
|
|
121
|
+
? `ital,wght@0,${weights.join(';0,')};1,${weights.join(';1,')}`
|
|
122
|
+
: `wght@${weights.join(';')}`;
|
|
123
|
+
|
|
124
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encodedFont}:${weightsParam}&display=swap`;
|
|
125
|
+
|
|
126
|
+
// Add a data attribute for easy identification
|
|
127
|
+
link.setAttribute('data-font-loader', primaryFont);
|
|
128
|
+
|
|
129
|
+
link.onload = () => {
|
|
130
|
+
PDS.log("log", `Successfully loaded font "${primaryFont}"`);
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
link.onerror = () => {
|
|
135
|
+
PDS.log("warn", `Failed to load font "${primaryFont}" from Google Fonts`);
|
|
136
|
+
reject(new Error(`Failed to load font: ${primaryFont}`));
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
document.head.appendChild(link);
|
|
140
|
+
|
|
141
|
+
// Set a timeout to prevent hanging indefinitely
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
if (!isFontAvailable(primaryFont)) {
|
|
144
|
+
PDS.log("warn", `Font "${primaryFont}" did not load within timeout`);
|
|
145
|
+
}
|
|
146
|
+
resolve(); // Resolve anyway to not block the application
|
|
147
|
+
}, 5000);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Loads fonts for all font families in a typography config
|
|
153
|
+
* @param {Object} typographyConfig - Typography configuration object
|
|
154
|
+
* @returns {Promise<void>}
|
|
155
|
+
*/
|
|
156
|
+
export async function loadTypographyFonts(typographyConfig) {
|
|
157
|
+
if (!typographyConfig) {
|
|
158
|
+
return Promise.resolve();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fontFamilies = new Set();
|
|
162
|
+
|
|
163
|
+
// Collect all font families from the config
|
|
164
|
+
if (typographyConfig.fontFamilyHeadings) {
|
|
165
|
+
fontFamilies.add(typographyConfig.fontFamilyHeadings);
|
|
166
|
+
}
|
|
167
|
+
if (typographyConfig.fontFamilyBody) {
|
|
168
|
+
fontFamilies.add(typographyConfig.fontFamilyBody);
|
|
169
|
+
}
|
|
170
|
+
if (typographyConfig.fontFamilyMono) {
|
|
171
|
+
fontFamilies.add(typographyConfig.fontFamilyMono);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Load all fonts in parallel
|
|
175
|
+
const loadPromises = Array.from(fontFamilies).map(fontFamily =>
|
|
176
|
+
loadGoogleFont(fontFamily).catch(err => {
|
|
177
|
+
PDS.log("warn", `Could not load font: ${fontFamily}`, err);
|
|
178
|
+
// Don't fail the whole operation if one font fails
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await Promise.all(loadPromises);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Removes previously loaded Google Fonts
|
|
187
|
+
* @param {string} fontName - Optional font name to remove. If not specified, removes all.
|
|
188
|
+
*/
|
|
189
|
+
export function unloadGoogleFont(fontName = null) {
|
|
190
|
+
const selector = fontName
|
|
191
|
+
? `link[data-font-loader="${fontName}"]`
|
|
192
|
+
: 'link[data-font-loader]';
|
|
193
|
+
|
|
194
|
+
const links = document.querySelectorAll(selector);
|
|
195
|
+
links.forEach(link => link.remove());
|
|
196
|
+
|
|
197
|
+
if (fontName) {
|
|
198
|
+
PDS.log("log", `Unloaded font "${fontName}"`);
|
|
199
|
+
} else {
|
|
200
|
+
PDS.log("log", `Unloaded ${links.length} font(s)`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
function normalizeLocaleTag(locale) {
|
|
2
|
+
return String(locale || "").trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function toBaseLocale(locale) {
|
|
6
|
+
return normalizeLocaleTag(locale).split("-")[0] || "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toLocaleList(locales, fallbackLocale) {
|
|
10
|
+
const output = [];
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
|
|
13
|
+
const add = (locale) => {
|
|
14
|
+
const normalized = normalizeLocaleTag(locale);
|
|
15
|
+
if (!normalized || seen.has(normalized)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
seen.add(normalized);
|
|
20
|
+
output.push(normalized);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(locales)) {
|
|
24
|
+
locales.forEach(add);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
add(fallbackLocale);
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeAliasMap(aliases = {}) {
|
|
32
|
+
const normalized = {};
|
|
33
|
+
|
|
34
|
+
if (!aliases || typeof aliases !== "object") {
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Object.entries(aliases).forEach(([key, values]) => {
|
|
39
|
+
const normalizedKey = normalizeLocaleTag(key);
|
|
40
|
+
if (!normalizedKey) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const list = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(values)) {
|
|
48
|
+
values.forEach((value) => {
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const dedupeKey = trimmed.toLowerCase();
|
|
59
|
+
if (seen.has(dedupeKey)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
seen.add(dedupeKey);
|
|
64
|
+
list.push(trimmed);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
normalized[normalizedKey] = list;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeBundleShape(bundle) {
|
|
75
|
+
if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return bundle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeBasePath(basePath) {
|
|
83
|
+
const raw = typeof basePath === "string" ? basePath.trim() : "";
|
|
84
|
+
if (!raw) {
|
|
85
|
+
return "/assets/locales";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (raw === "/") {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return raw.replace(/\/+$/, "");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildCandidateLocales({ locale, effectiveLocale, defaultLocale, aliases }) {
|
|
96
|
+
const candidates = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
|
|
99
|
+
const add = (candidate) => {
|
|
100
|
+
if (typeof candidate !== "string") {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const trimmed = candidate.trim();
|
|
105
|
+
if (!trimmed) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dedupeKey = trimmed.toLowerCase();
|
|
110
|
+
if (seen.has(dedupeKey)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
seen.add(dedupeKey);
|
|
115
|
+
candidates.push(trimmed);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const addAliases = (aliasKey) => {
|
|
119
|
+
const values = Array.isArray(aliases?.[aliasKey]) ? aliases[aliasKey] : [];
|
|
120
|
+
if (!values.length) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const prioritized = [...values].sort((a, b) => {
|
|
125
|
+
const aSpecific = String(a || "").includes("-");
|
|
126
|
+
const bSpecific = String(b || "").includes("-");
|
|
127
|
+
if (aSpecific === bSpecific) return 0;
|
|
128
|
+
return aSpecific ? -1 : 1;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
prioritized.forEach(add);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const normalizedRequested = normalizeLocaleTag(locale);
|
|
135
|
+
const requestedBase = toBaseLocale(normalizedRequested);
|
|
136
|
+
const effectiveBase = toBaseLocale(effectiveLocale);
|
|
137
|
+
|
|
138
|
+
addAliases(normalizedRequested);
|
|
139
|
+
addAliases(requestedBase);
|
|
140
|
+
addAliases(effectiveLocale);
|
|
141
|
+
addAliases(effectiveBase);
|
|
142
|
+
|
|
143
|
+
add(locale);
|
|
144
|
+
add(normalizedRequested);
|
|
145
|
+
add(requestedBase);
|
|
146
|
+
add(effectiveLocale);
|
|
147
|
+
add(effectiveBase);
|
|
148
|
+
|
|
149
|
+
if (effectiveLocale !== defaultLocale) {
|
|
150
|
+
addAliases(defaultLocale);
|
|
151
|
+
add(defaultLocale);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return candidates;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Create a localization config that lazy-loads locale JSON resources.
|
|
159
|
+
*
|
|
160
|
+
* JSON files are expected at:
|
|
161
|
+
* - `${basePath}/{locale}.json`
|
|
162
|
+
*
|
|
163
|
+
* Bundle format can be either:
|
|
164
|
+
* - `{ "Key": "Translated" }`
|
|
165
|
+
* - `{ "Key": { "content": "Translated" } }`
|
|
166
|
+
*
|
|
167
|
+
* @param {{
|
|
168
|
+
* locale?: string,
|
|
169
|
+
* locales?: string[],
|
|
170
|
+
* basePath?: string,
|
|
171
|
+
* aliases?: Record<string, string[]>,
|
|
172
|
+
* requestInit?: RequestInit,
|
|
173
|
+
* cache?: Map<string, Record<string, string | { content?: string }>>,
|
|
174
|
+
* }} [options]
|
|
175
|
+
* @returns {{
|
|
176
|
+
* locale: string,
|
|
177
|
+
* locales: string[],
|
|
178
|
+
* provider: {
|
|
179
|
+
* locales: string[],
|
|
180
|
+
* loadLocale: (context: { locale: string }) => Promise<Record<string, string | { content?: string }>>,
|
|
181
|
+
* },
|
|
182
|
+
* }}
|
|
183
|
+
*/
|
|
184
|
+
export function createJSONLocalization(options = {}) {
|
|
185
|
+
const defaultLocale = normalizeLocaleTag(options?.locale || "en") || "en";
|
|
186
|
+
const locales = toLocaleList(options?.locales, defaultLocale);
|
|
187
|
+
const localeSet = new Set(locales);
|
|
188
|
+
const aliases = normalizeAliasMap(options?.aliases || {});
|
|
189
|
+
const cache = options?.cache instanceof Map ? options.cache : new Map();
|
|
190
|
+
const basePath = normalizeBasePath(options?.basePath);
|
|
191
|
+
const requestInit =
|
|
192
|
+
options?.requestInit && typeof options.requestInit === "object"
|
|
193
|
+
? options.requestInit
|
|
194
|
+
: {};
|
|
195
|
+
|
|
196
|
+
const resolveEffectiveLocale = (locale) => {
|
|
197
|
+
const normalized = normalizeLocaleTag(locale);
|
|
198
|
+
if (!normalized) {
|
|
199
|
+
return defaultLocale;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (localeSet.has(normalized)) {
|
|
203
|
+
return normalized;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const base = toBaseLocale(normalized);
|
|
207
|
+
if (localeSet.has(base)) {
|
|
208
|
+
return base;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return defaultLocale;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const loadLocale = async ({ locale }) => {
|
|
215
|
+
const effectiveLocale = resolveEffectiveLocale(locale);
|
|
216
|
+
if (cache.has(effectiveLocale)) {
|
|
217
|
+
return cache.get(effectiveLocale) || {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (effectiveLocale === defaultLocale) {
|
|
221
|
+
const defaultBundle = {};
|
|
222
|
+
cache.set(effectiveLocale, defaultBundle);
|
|
223
|
+
return defaultBundle;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof fetch !== "function") {
|
|
227
|
+
const emptyBundle = {};
|
|
228
|
+
cache.set(effectiveLocale, emptyBundle);
|
|
229
|
+
return emptyBundle;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const candidates = buildCandidateLocales({
|
|
233
|
+
locale,
|
|
234
|
+
effectiveLocale,
|
|
235
|
+
defaultLocale,
|
|
236
|
+
aliases,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
for (const candidate of candidates) {
|
|
240
|
+
try {
|
|
241
|
+
const resourcePath = basePath
|
|
242
|
+
? `${basePath}/${candidate}.json`
|
|
243
|
+
: `/${candidate}.json`;
|
|
244
|
+
const response = await fetch(resourcePath, {
|
|
245
|
+
headers: {
|
|
246
|
+
Accept: "application/json",
|
|
247
|
+
},
|
|
248
|
+
...requestInit,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const bundle = normalizeBundleShape(await response.json());
|
|
256
|
+
cache.set(effectiveLocale, bundle);
|
|
257
|
+
return bundle;
|
|
258
|
+
} catch (error) {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const emptyBundle = {};
|
|
262
|
+
cache.set(effectiveLocale, emptyBundle);
|
|
263
|
+
return emptyBundle;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
locale: defaultLocale,
|
|
268
|
+
locales: [...locales],
|
|
269
|
+
provider: {
|
|
270
|
+
locales: [...locales],
|
|
271
|
+
loadLocale,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|