UNPKG

svelte-i18n-lingui

Version:

Add i18n to Svelte/Sveltekit projects using Lingui, using message as the catalog id

180 lines (161 loc) 5.77 kB
/** @typedef {{ match(filename: string) => boolean, extract(filename: string, code: string, onMessageExtracted: (msg: ExtractedMessage) => void, ctx?: ExtractorCtx)=> Promise<void> | void }} ExtractorType */ // import fs from 'fs'; import { parse } from 'svelte/compiler'; import { walk } from 'estree-walker-ts'; import { parse as tsParse } from '@typescript-eslint/typescript-estree'; import { generateMessageId } from './generateMessageId.js'; const extractFromTaggedTemplate = (node, filename, onMessageExtracted) => { // `node.quasi.loc` is for extraction from svelte files, and `node.quasi` is for extraction from js/ts files const { start } = node.quasi.loc; const rawQuasis = node.quasi.quasis.map((q) => q.value.raw); let message = rawQuasis[0]; rawQuasis.slice(1).forEach((q, i) => { message += `{${i}}${q}`; }); onMessageExtracted({ id: generateMessageId(message), message, origin: [filename, start.line, start.column], placeholders: {}, }); }; const extractFromCallExpression = (node, filename, onMessageExtracted) => { const { start } = node.loc; const { properties } = node.arguments[0]; const messageProperty = properties.find((p) => p.key.name === 'message'); if (messageProperty === undefined) { throw new Error('MessageDescriptor should contain a message property'); } const message = messageProperty.value.value; // Only extract if message is a string literal, otherwise skip the node as it's not a valid MessageDescriptor if (message) { const context = properties.find((p) => p.key.name === 'context')?.value.value; const comment = properties.find((p) => p.key.name === 'comment')?.value.value; onMessageExtracted({ id: generateMessageId(message, context), message, context, comment, origin: [filename, start.line, start.column], placeholders: {}, }); } }; const extractTags = (tags, node, filename, onMessageExtracted) => { if (node.type === 'TaggedTemplateExpression' && tags.includes(node.tag.name)) { extractFromTaggedTemplate(node, filename, onMessageExtracted); } else if ( node.type === 'CallExpression' && tags.includes(node.callee.name) && node.arguments[0].type === 'ObjectExpression' ) { extractFromCallExpression(node, filename, onMessageExtracted); } }; const extractPlurals = (tags, node, filename, onMessageExtracted) => { if ( node.type === 'CallExpression' && tags.includes(node.callee.name) && node.arguments[1].type === 'ObjectExpression' ) { const { start } = node.loc; const { properties } = node.arguments[1]; const message = `{num, plural, ${properties // key will use the "name" property for normal properties, and "value" property for exact matches .map((p) => `${p.key.name ?? p.key.value} {${p.value.value}}`) .join(' ')}}`; onMessageExtracted({ id: generateMessageId(message), message, origin: [filename, start.line, start.column], placeholders: {}, // The actual number's value doesn't matter when extracting so we don't have to supply it }); } }; const extractPluralMessages = (tags, node, filename, onMessageExtracted) => { if ( node.type === 'CallExpression' && tags.includes(node.callee.name) && node.arguments[0].type === 'ObjectExpression' ) { const { start } = node.loc; const { properties } = node.arguments[0]; const message = `{num, plural, ${properties // key will use the "name" property for normal properties, and "value" property for exact matches .map((p) => `${p.key.name ?? p.key.value} {${p.value.value}}`) .join(' ')}}`; onMessageExtracted({ id: generateMessageId(message), message, origin: [filename, start.line, start.column], placeholders: {}, }); } }; const extractComponent = (node, filename, onMessageExtracted) => { if (node.type === 'Component' && node.name === 'T') { const { start } = node; // FIXME: Find out why Loc is not printed here, causing this to be incorrect const { attributes } = node; const message = attributes.find((a) => a.name === 'msg')?.value[0].data; const context = attributes.find((a) => a.name === 'ctx')?.value[0].data; const comment = attributes.find((a) => a.name === 'cmt')?.value[0].data; onMessageExtracted({ id: generateMessageId(message, context), message, context, comment, origin: [filename, start.line, start.column], placeholders: {}, }); } }; /** * @type {ExtractorType} */ export const svelteExtractor = { match(filename) { return filename.endsWith('.svelte'); }, async extract(filename, source, onMessageExtracted, _ctx) { try { const ast = parse(source, { filename, modern: true }); walk(ast, { enter(node, _parent, _prop, _index) { extractTags(['$t', 'msg'], node, filename, onMessageExtracted); extractPlurals(['$plural'], node, filename, onMessageExtracted); extractPluralMessages(['msgPlural'], node, filename, onMessageExtracted); extractComponent(node, filename, onMessageExtracted); }, }); } catch (err) { console.log(`Error at ${filename}:`, err); } }, }; /** * @type {ExtractorType} */ export const jstsExtractor = { match(filename) { return filename.endsWith('.js') || filename.endsWith('.ts'); }, async extract(filename, source, onMessageExtracted, _ctx) { try { const ast = tsParse(source, { filePath: filename, loc: true, }); // fs.writeFileSync('ast.json', JSON.stringify(ast, null, 2)); walk(ast, { enter(node, _parent, _prop, _index) { extractTags(['gt', 'msg'], node, filename, onMessageExtracted); extractPlurals(['gPlural'], node, filename, onMessageExtracted); extractPluralMessages(['msgPlural'], node, filename, onMessageExtracted); }, }); } catch (err) { console.log(`Error at ${filename}:`, err); } }, };