Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
97 lines
3.3 KiB
Plaintext
97 lines
3.3 KiB
Plaintext
import path from 'path';
|
|
import compile from 'icu-minify/compile';
|
|
import { getFormatExtension, resolveCodec } from './format/index.js';
|
|
import { setNestedProperty } from './utils.js';
|
|
|
|
// The module scope is safe for some caching, but Next.js can
|
|
// create multiple loader instances so don't expect a singleton.
|
|
let cachedCodec = null;
|
|
const messageCacheByCatalog = new Map();
|
|
function getMessageCache(catalogId) {
|
|
let cache = messageCacheByCatalog.get(catalogId);
|
|
if (!cache) {
|
|
cache = new Map();
|
|
messageCacheByCatalog.set(catalogId, cache);
|
|
}
|
|
return cache;
|
|
}
|
|
async function getCodec(options, projectRoot) {
|
|
if (!cachedCodec) {
|
|
cachedCodec = await resolveCodec(options.messages.format, projectRoot);
|
|
}
|
|
return cachedCodec;
|
|
}
|
|
|
|
/**
|
|
* Parses and optimizes catalog files.
|
|
*
|
|
* Note that if we use a dynamic import like `import(`${locale}.json`)`, then
|
|
* the loader will optimistically run for all candidates in this folder (both
|
|
* during dev as well as at build time).
|
|
*/
|
|
function catalogLoader(source) {
|
|
const options = this.getOptions();
|
|
const callback = this.async();
|
|
const extension = getFormatExtension(options.messages.format);
|
|
getCodec(options, this.rootContext).then(codec => {
|
|
const locale = path.basename(this.resourcePath, extension);
|
|
let outputString;
|
|
if (options.messages.precompile) {
|
|
const decoded = codec.decode(source, {
|
|
locale
|
|
});
|
|
const cache = getMessageCache(this.resourcePath);
|
|
const precompiled = precompileMessages(decoded, cache);
|
|
outputString = JSON.stringify(precompiled);
|
|
} else {
|
|
outputString = codec.toJSONString(source, {
|
|
locale
|
|
});
|
|
}
|
|
|
|
// https://v8.dev/blog/cost-of-javascript-2019#json
|
|
const result = `export default JSON.parse(${JSON.stringify(outputString)});`;
|
|
callback(null, result);
|
|
}).catch(callback);
|
|
}
|
|
|
|
/**
|
|
* Recursively precompiles all ICU message strings in a messages object
|
|
* using icu-minify/compile for smaller runtime bundles.
|
|
*/
|
|
function precompileMessages(messages, cache) {
|
|
const result = {};
|
|
const cacheKeysToEvict = new Set(cache.keys());
|
|
for (const message of messages) {
|
|
cacheKeysToEvict.delete(message.id);
|
|
const messageValue = message.message;
|
|
if (Array.isArray(messageValue)) {
|
|
throw new Error(`Message at \`${message.id}\` resolved to an array, but only strings are supported. See https://next-intl.dev/docs/usage/translations#arrays-of-messages`);
|
|
}
|
|
if (typeof messageValue === 'object') {
|
|
throw new Error(`Message at \`${message.id}\` resolved to \`${typeof messageValue}\`, but only strings are supported. Use a \`.\` to retrieve nested messages. See https://next-intl.dev/docs/usage/translations#structuring-messages`);
|
|
}
|
|
const cachedEntry = cache.get(message.id);
|
|
const hasCacheMatch = cachedEntry?.messageValue === messageValue;
|
|
let compiledMessage;
|
|
if (hasCacheMatch) {
|
|
compiledMessage = cachedEntry.compiledMessage;
|
|
} else {
|
|
compiledMessage = compile(messageValue);
|
|
cache.set(message.id, {
|
|
compiledMessage,
|
|
messageValue
|
|
});
|
|
}
|
|
setNestedProperty(result, message.id, compiledMessage);
|
|
}
|
|
|
|
// Evict unused cache entries
|
|
for (const cachedId of cacheKeysToEvict) {
|
|
cache.delete(cachedId);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export { catalogLoader as default };
|