UNPKG

next-intl

Version:

Internationalization (i18n) for Next.js

97 lines (90 loc) 3.36 kB
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 };