/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import MagicString from 'magic-string'; import { assertNever } from '../utils.js'; import type { Locale, ILocale } from '../../../locales/index.js'; import type { TextModification } from '../locale-inliner.js'; import type { Logger } from '../logger.js'; export function applyWithLocale( sourceCode: MagicString, modifications: TextModification[], localeName: string, localeJson: Locale, fileLogger: Logger, ) { for (const modification of modifications) { switch (modification.type) { case 'delete': sourceCode.remove(modification.begin, modification.end); break; case 'insert': sourceCode.appendRight(modification.begin, modification.text); break; case 'replace': sourceCode.update(modification.begin, modification.end, modification.text); break; case 'localized': { const accessed = getPropertyByPath(localeJson, modification.localizationKey); if (accessed == null) { fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`); } sourceCode.update(modification.begin, modification.end, JSON.stringify(accessed)); break; } case 'parameterized-function': { const accessed = getPropertyByPath(localeJson, modification.localizationKey); let replacement: string; if (typeof accessed === 'string') { replacement = formatFunction(accessed); } else if (typeof accessed === 'object' && accessed !== null) { replacement = `({${Object.entries(accessed).map(([key, value]) => `${JSON.stringify(key)}:${formatFunction(value)}`).join(',')}})`; } else { fileLogger.warn(`Cannot find localization key ${modification.localizationKey.join('.')}`); replacement = '(() => "")'; // placeholder for missing locale } sourceCode.update(modification.begin, modification.end, replacement); break; function formatFunction(format: string): string { const params = new Set(); const components: string[] = []; let lastIndex = 0; for (const match of format.matchAll(/\{(.+?)}/g)) { const [fullMatch, paramName] = match; if (lastIndex < match.index) { components.push(JSON.stringify(format.slice(lastIndex, match.index))); } params.add(paramName); components.push(paramName); lastIndex = match.index + fullMatch.length; } components.push(JSON.stringify(format.slice(lastIndex))); // we replace with `(({name,count})=>(name+count+"some"))` const paramList = Array.from(params).join(','); let body = components.filter(x => x !== '""').join('+'); if (body === '') body = '""'; // if the body is empty, we return empty string return `(({${paramList}})=>(${body}))`; } } case 'locale-name': { sourceCode.update(modification.begin, modification.end, modification.literal ? JSON.stringify(localeName) : localeName); break; } case 'locale-json': { // locale-json is inlined to place where initialize module-level variable which is executed only once. // In such case we can use JSON.parse to speed up the parsing script. // https://v8.dev/blog/cost-of-javascript-2019#json sourceCode.update(modification.begin, modification.end, `JSON.parse(${JSON.stringify(JSON.stringify(localeJson))})`); break; } default: { assertNever(modification); } } } } function getPropertyByPath(localeJson: ILocale, localizationKey: string[]): string | object | null { if (localizationKey.length === 0) return localeJson; let current: ILocale | string = localeJson; for (const key of localizationKey) { if (typeof current !== 'object' || !(key in current)) { return null; // Key not found } current = current[key]; } return current; }