next-intl
Version:
Internationalization (i18n) for Next.js
97 lines (90 loc) • 3.36 kB
JavaScript
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 };