svelte-intl-precompile
Version:
I18n library for Svelte.js that analyzes your keys at build time for max performance and minimal footprint
239 lines (196 loc) • 7.2 kB
JavaScript
;
const path = require('path');
const fs = require('fs/promises');
const babel = require('@babel/core');
const buildICUPlugin = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('babel-plugin-precompile-intl'));
const pathStartsWith = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('path-starts-with'))
const intlPrecompiler = buildICUPlugin('svelte-intl-precompile');
function transformCode(code, options) {
return babel.transform(code, { ...options, plugins: [intlPrecompiler] }).code;
}
exports.transformCode = transformCode
const transformScript = (content) => content
const transformJSON = async (content) => {
const { default: JSON5 } = await import('json5');
return `export default ${JSON.stringify(JSON5.parse(content))}`;
}
const transformYaml = async (content, { filename }) => {
const { load } = await import('js-yaml');
return `export default ${JSON.stringify(load(content, { filename }))}`;
}
const stdTransformers = {
// order matters as it defines which extensions are tried first
// when looking for a matching file for a locale
'.js': transformScript,
'.ts': transformScript,
'.mjs': transformScript,
// use json5 loader for json files
'.json': transformJSON,
'.json5': transformJSON,
'.yaml': transformYaml,
'.yml': transformYaml,
};
function svelteIntlPrecompile(localesRoot, prefixOrOptions) {
const {
prefix = '$locales',
transformers: customTransformers,
exclude,
} = typeof prefixOrOptions === 'string'
? { prefix: prefixOrOptions }
: { ...prefixOrOptions };
const resolvedPath = path.resolve(localesRoot);
const transformers = { ...stdTransformers, ...customTransformers }
async function loadPrefixModule() {
const code = [
`import { register } from 'svelte-intl-precompile'`,
`export function registerAll() {`
// register('en', () => import('$locales/en'))
];
const availableLocales = [];
const filesInLocalesFolder = await fs.readdir(localesRoot)
const excludeFn = typeof exclude === 'function' ? exclude : (exclude instanceof RegExp ? (s) => exclude.test(s) : () => false);
const languageFiles = filesInLocalesFolder.filter(name => !excludeFn(name));
// add register calls for each found locale
for (const file of languageFiles) {
const extname = path.extname(file);
if (transformers[extname]) {
const locale = path.basename(file, extname);
// ensure we register each locale only once
// there shouldn't be more than one file each locale
// our load(id) method only ever loads the first found
// file for a locale and it makes no sense to register
// more than one file per locale
if (!availableLocales.includes(locale)) {
availableLocales.push(locale);
code.push(
` register(${JSON.stringify(locale)}, () => import(${
JSON.stringify(`${prefix}/${locale}`)
}))`,
)
}
}
}
// Sort locales that more specific locales come first
// 'en-US' comes before 'en'
availableLocales.sort((a, b) => {
const order = a.split('-').length - b.split('-').length
if (order) return order
return a.localeCompare(b, 'en')
})
code.push(
`}`,
`export const availableLocales = ${JSON.stringify(availableLocales)}`,
);
return code.join('\n');
}
async function tranformLocale(
content,
{
filename,
extname = path.extname(filename),
basename = path.basename(filename, extname),
transform = transformers[extname] || transformScript
}
) {
const code = await transform(content, { filename, basename, extname });
return transformCode(code, { filename });
}
async function findLocale(basename) {
const filebase = path.resolve(localesRoot, basename);
// lazy loaded here because strip-bom is an es module
// and this file could be a cjs module
// by using dynamic import we can load es modules
const { default: stripBom } = await import('strip-bom');
for await (const [extname, transform] of Object.entries(transformers)) {
const filename = filebase + extname;
try {
const content = await fs.readFile(filename, { encoding: 'utf-8' });
return tranformLocale(stripBom(content), { filename, basename, extname, transform });
} catch (error) {
// incase the file did not exist try next transformer
// otherwise propagate the error
if (error.code !== 'ENOENT') {
throw error;
}
}
}
}
return {
name: 'svelte-intl-precompile', // required, will show up in warnings and errors
enforce: 'pre',
configureServer(server) {
const { ws, watcher, moduleGraph } = server;
// listen to vite files watcher
watcher.on('change', (file) => {
file = path.relative('', file);
// check if file changed is a locale
if (pathStartsWith(file, localesRoot)) {
// invalidate $locales/<locales><extname> modules
const name = `${prefix}/${path.basename(file, path.extname(file))}`;
// Alltough we are normalizing the module names
// $locales/en.js -> $locales/en
// we check all configured extensions just to be sure
// '.js', '.ts', '.json', ..., and '' ($locales/en)
for (const extname of [...Object.keys(transformers), '']) {
// check if locale file is in vite cache
const localeModule = moduleGraph.getModuleById(`${name}${extname}`);
if (localeModule) {
moduleGraph.invalidateModule(localeModule);
}
}
// invalidate $locales module
const prefixModule = moduleGraph.getModuleById(prefix)
if (prefixModule) {
moduleGraph.invalidateModule(prefixModule);
}
// trigger hmr
ws.send({ type: 'full-reload', path: '*' });
}
})
},
resolveId(id) {
if (id === prefix || id.startsWith(`${prefix}/`)) {
const extname = path.extname(id);
// "normalize" module id to have no extension
// $locales/en.js -> $locales/en
// we do this as the extension is ignored
// when loading a locale
// we always try to find a locale file by its basename
// and adding an extension from transformers
// additionally this prevents loading the same module/locale
// several times with different extensions
const normalized = extname
? id.slice(0, -extname.length)
: id
return normalized;
}
},
load(id) {
// allow to auto register locales by calling registerAll from $locales module
// import { registerAll, availableLocales } from '$locales'
if (id === prefix) {
return loadPrefixModule();
}
// import en from '$locales/en'
// import en from '$locales/en.js'
if (id.startsWith(`${prefix}/`)) {
const extname = path.extname(id);
// $locales/en -> en
// $locales/en.js -> en
// $locales/en.ts -> en
const locale = extname
? id.slice(`${prefix}/`.length, -extname.length)
: id.slice(`${prefix}/`.length)
return findLocale(locale);
}
},
transform(content, id) {
// import locale from '../locales/en.js'
if (pathStartsWith(id, resolvedPath)) {
return tranformLocale(content, { filename: id });
}
}
}
}
svelteIntlPrecompile.transformCode = transformCode;
module.exports = svelteIntlPrecompile;