Migrate from tsup (deprecated) to tsdown

tsup is deprecated in favor of tsdown.

Follow https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, and many (local) tries were I was not really happy with the files generated by tsdown/rolldown, we are finally having something extra good!

We have the benefits from https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, but without their drawbacks. The code correctly follow the `es2022` target and does not do anything weird anymore with static properties (needed by controllers).

I (Claude) added a plugin to remove the region and JSDoc comments (except if they contain `@deprecated``), since I think we want to keep the code non-minified.
This commit is contained in:
Hugo Alliaume
2026-02-28 08:36:49 +01:00
parent 12d5ad4739
commit b5f691f37c
3 changed files with 207 additions and 284 deletions

View File

@@ -1,42 +1,36 @@
type MessageId = string;
type DomainType = string;
type LocaleType = string;
type TranslationsType = Record<DomainType, { parameters: ParametersType }>;
type TranslationsType = Record<DomainType, {
parameters: ParametersType;
}>;
type NoParametersType = Record<string, never>;
type ParametersType = Record<string, string | number | Date> | NoParametersType;
type RemoveIntlIcuSuffix<T> = T extends `${infer U}+intl-icu` ? U : T;
type DomainsOf<M> = M extends Message<infer Translations, LocaleType> ? keyof Translations : never;
type LocaleOf<M> = M extends Message<TranslationsType, infer Locale> ? Locale : never;
type ParametersOf<M, D extends DomainType> =
M extends Message<infer Translations, LocaleType>
? Translations[D] extends { parameters: infer Parameters }
? Parameters
: never
: never;
type ParametersOf<M, D extends DomainType> = M extends Message<infer Translations, LocaleType> ? Translations[D] extends {
parameters: infer Parameters;
} ? Parameters : never : never;
interface Message<Translations extends TranslationsType, Locale extends LocaleType> {
translations: {
[domain in DomainType]: {
[locale in Locale]: string;
};
};
translations: { [domain in DomainType]: { [locale in Locale]: string } };
}
type Messages = Record<MessageId, Message<TranslationsType, LocaleType>>;
declare function getDefaultLocale(): LocaleType;
declare function createTranslator<TMessages extends Messages>({ messages, locale, localeFallbacks, throwWhenNotFound, }: {
messages: TMessages;
locale?: LocaleType;
localeFallbacks?: Record<LocaleType, LocaleType>;
throwWhenNotFound?: boolean;
declare function createTranslator<TMessages extends Messages>({
messages,
locale,
localeFallbacks,
throwWhenNotFound
}: {
messages: TMessages;
locale?: LocaleType;
localeFallbacks?: Record<LocaleType, LocaleType>;
throwWhenNotFound?: boolean;
}): {
setLocale(locale: LocaleType): void;
getLocale(): LocaleType;
setThrowWhenNotFound(throwWhenNotFound: boolean): void;
trans<TMessageId extends keyof TMessages & MessageId, TMessage extends TMessages[TMessageId], TDomain extends DomainsOf<TMessage>, TParameters extends ParametersOf<TMessage, TDomain>>(id: TMessageId, parameters?: TParameters, domain?: RemoveIntlIcuSuffix<TDomain> | undefined, locale?: LocaleOf<TMessage>): string;
setLocale(locale: LocaleType): void;
getLocale(): LocaleType;
setThrowWhenNotFound(throwWhenNotFound: boolean): void;
trans<TMessageId extends keyof TMessages & MessageId, TMessage extends TMessages[TMessageId], TDomain extends DomainsOf<TMessage>, TParameters extends ParametersOf<TMessage, TDomain>>(id: TMessageId, parameters?: TParameters, domain?: RemoveIntlIcuSuffix<TDomain> | undefined, locale?: LocaleOf<TMessage>): string;
};
export { type DomainType, type DomainsOf, type LocaleOf, type LocaleType, type Message, type MessageId, type Messages, type NoParametersType, type ParametersOf, type ParametersType, type RemoveIntlIcuSuffix, type TranslationsType, createTranslator, getDefaultLocale };
export { DomainType, DomainsOf, LocaleOf, LocaleType, Message, MessageId, Messages, NoParametersType, ParametersOf, ParametersType, RemoveIntlIcuSuffix, TranslationsType, createTranslator, getDefaultLocale };

View File

@@ -1,266 +1,195 @@
// src/utils.ts
import { IntlMessageFormat } from "intl-messageformat";
function strtr(string, replacePairs) {
const regex = Object.entries(replacePairs).map(([from]) => {
return from.replace(/([-[\]{}()*+?.\\^$|#,])/g, "\\$1");
});
if (regex.length === 0) {
return string;
}
return string.replace(new RegExp(regex.join("|"), "g"), (matched) => replacePairs[matched].toString());
const regex = Object.entries(replacePairs).map(([from]) => {
return from.replace(/([-[\]{}()*+?.\\^$|#,])/g, "\\$1");
});
if (regex.length === 0) return string;
return string.replace(new RegExp(regex.join("|"), "g"), (matched) => replacePairs[matched].toString());
}
// src/formatters/formatter.ts
function format(id, parameters, locale) {
if (null === id || "" === id) {
return "";
}
if (typeof parameters["%count%"] === "undefined" || Number.isNaN(parameters["%count%"])) {
return strtr(id, parameters);
}
const number = Number(parameters["%count%"]);
let parts = [];
if (/^\|+$/.test(id)) {
parts = id.split("|");
} else {
parts = id.match(/(?:\|\||[^|])+/g) || [];
}
const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s;
const standardRules = [];
for (let part of parts) {
part = part.trim().replace(/\|\|/g, "|");
const matches = part.match(intervalRegex);
if (matches) {
const matchGroups = matches.groups || {};
if (matches[2]) {
for (const n of matches[3].split(",")) {
if (number === Number(n)) {
return strtr(matchGroups.message, parameters);
}
}
} else {
const leftNumber = "-Inf" === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left);
const rightNumber = ["Inf", "+Inf"].includes(matchGroups.right) ? Number.POSITIVE_INFINITY : Number(matchGroups.right);
if (("[" === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) && ("]" === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) {
return strtr(matchGroups.message, parameters);
}
}
} else {
const ruleMatch = part.match(/^\w+:\s*(.*?)$/);
standardRules.push(ruleMatch ? ruleMatch[1] : part);
}
}
const position = getPluralizationRule(number, locale);
if (typeof standardRules[position] === "undefined") {
if (1 === parts.length && typeof standardRules[0] !== "undefined") {
return strtr(standardRules[0], parameters);
}
throw new Error(
`Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`
);
}
return strtr(standardRules[position], parameters);
if (null === id || "" === id) return "";
if (typeof parameters["%count%"] === "undefined" || Number.isNaN(parameters["%count%"])) return strtr(id, parameters);
const number = Number(parameters["%count%"]);
let parts = [];
if (/^\|+$/.test(id)) parts = id.split("|");
else parts = id.match(/(?:\|\||[^|])+/g) || [];
const intervalRegex = /^(?<interval>({\s*(-?\d+(\.\d+)?[\s*,\s*\-?\d+(.\d+)?]*)\s*})|(?<left_delimiter>[[\]])\s*(?<left>-Inf|-?\d+(\.\d+)?)\s*,\s*(?<right>\+?Inf|-?\d+(\.\d+)?)\s*(?<right_delimiter>[[\]]))\s*(?<message>.*?)$/s;
const standardRules = [];
for (let part of parts) {
part = part.trim().replace(/\|\|/g, "|");
const matches = part.match(intervalRegex);
if (matches) {
const matchGroups = matches.groups || {};
if (matches[2]) {
for (const n of matches[3].split(",")) if (number === Number(n)) return strtr(matchGroups.message, parameters);
} else {
const leftNumber = "-Inf" === matchGroups.left ? Number.NEGATIVE_INFINITY : Number(matchGroups.left);
const rightNumber = ["Inf", "+Inf"].includes(matchGroups.right) ? Number.POSITIVE_INFINITY : Number(matchGroups.right);
if (("[" === matchGroups.left_delimiter ? number >= leftNumber : number > leftNumber) && ("]" === matchGroups.right_delimiter ? number <= rightNumber : number < rightNumber)) return strtr(matchGroups.message, parameters);
}
} else {
const ruleMatch = part.match(/^\w+:\s*(.*?)$/);
standardRules.push(ruleMatch ? ruleMatch[1] : part);
}
}
const position = getPluralizationRule(number, locale);
if (typeof standardRules[position] === "undefined") {
if (1 === parts.length && typeof standardRules[0] !== "undefined") return strtr(standardRules[0], parameters);
throw new Error(`Unable to choose a translation for "${id}" with locale "${locale}" for value "${number}". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %count% apples").`);
}
return strtr(standardRules[position], parameters);
}
function getPluralizationRule(number, locale) {
number = Math.abs(number);
let _locale = locale;
if (locale === "pt_BR" || locale === "en_US_POSIX") {
return 0;
}
_locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf("_")) : _locale;
switch (_locale) {
case "af":
case "bn":
case "bg":
case "ca":
case "da":
case "de":
case "el":
case "en":
case "en_US_POSIX":
case "eo":
case "es":
case "et":
case "eu":
case "fa":
case "fi":
case "fo":
case "fur":
case "fy":
case "gl":
case "gu":
case "ha":
case "he":
case "hu":
case "is":
case "it":
case "ku":
case "lb":
case "ml":
case "mn":
case "mr":
case "nah":
case "nb":
case "ne":
case "nl":
case "nn":
case "no":
case "oc":
case "om":
case "or":
case "pa":
case "pap":
case "ps":
case "pt":
case "so":
case "sq":
case "sv":
case "sw":
case "ta":
case "te":
case "tk":
case "ur":
case "zu":
return 1 === number ? 0 : 1;
case "am":
case "bh":
case "fil":
case "fr":
case "gun":
case "hi":
case "hy":
case "ln":
case "mg":
case "nso":
case "pt_BR":
case "ti":
case "wa":
return number < 2 ? 0 : 1;
case "be":
case "bs":
case "hr":
case "ru":
case "sh":
case "sr":
case "uk":
return 1 === number % 10 && 11 !== number % 100 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case "cs":
case "sk":
return 1 === number ? 0 : number >= 2 && number <= 4 ? 1 : 2;
case "ga":
return 1 === number ? 0 : 2 === number ? 1 : 2;
case "lt":
return 1 === number % 10 && 11 !== number % 100 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case "sl":
return 1 === number % 100 ? 0 : 2 === number % 100 ? 1 : 3 === number % 100 || 4 === number % 100 ? 2 : 3;
case "mk":
return 1 === number % 10 ? 0 : 1;
case "mt":
return 1 === number ? 0 : 0 === number || number % 100 > 1 && number % 100 < 11 ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
case "lv":
return 0 === number ? 0 : 1 === number % 10 && 11 !== number % 100 ? 1 : 2;
case "pl":
return 1 === number ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
case "cy":
return 1 === number ? 0 : 2 === number ? 1 : 8 === number || 11 === number ? 2 : 3;
case "ro":
return 1 === number ? 0 : 0 === number || number % 100 > 0 && number % 100 < 20 ? 1 : 2;
case "ar":
return 0 === number ? 0 : 1 === number ? 1 : 2 === number ? 2 : number % 100 >= 3 && number % 100 <= 10 ? 3 : number % 100 >= 11 && number % 100 <= 99 ? 4 : 5;
default:
return 0;
}
number = Math.abs(number);
let _locale = locale;
if (locale === "pt_BR" || locale === "en_US_POSIX") return 0;
_locale = _locale.length > 3 ? _locale.substring(0, _locale.indexOf("_")) : _locale;
switch (_locale) {
case "af":
case "bn":
case "bg":
case "ca":
case "da":
case "de":
case "el":
case "en":
case "en_US_POSIX":
case "eo":
case "es":
case "et":
case "eu":
case "fa":
case "fi":
case "fo":
case "fur":
case "fy":
case "gl":
case "gu":
case "ha":
case "he":
case "hu":
case "is":
case "it":
case "ku":
case "lb":
case "ml":
case "mn":
case "mr":
case "nah":
case "nb":
case "ne":
case "nl":
case "nn":
case "no":
case "oc":
case "om":
case "or":
case "pa":
case "pap":
case "ps":
case "pt":
case "so":
case "sq":
case "sv":
case "sw":
case "ta":
case "te":
case "tk":
case "ur":
case "zu": return 1 === number ? 0 : 1;
case "am":
case "bh":
case "fil":
case "fr":
case "gun":
case "hi":
case "hy":
case "ln":
case "mg":
case "nso":
case "pt_BR":
case "ti":
case "wa": return number < 2 ? 0 : 1;
case "be":
case "bs":
case "hr":
case "ru":
case "sh":
case "sr":
case "uk": return 1 === number % 10 && 11 !== number % 100 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case "cs":
case "sk": return 1 === number ? 0 : number >= 2 && number <= 4 ? 1 : 2;
case "ga": return 1 === number ? 0 : 2 === number ? 1 : 2;
case "lt": return 1 === number % 10 && 11 !== number % 100 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;
case "sl": return 1 === number % 100 ? 0 : 2 === number % 100 ? 1 : 3 === number % 100 || 4 === number % 100 ? 2 : 3;
case "mk": return 1 === number % 10 ? 0 : 1;
case "mt": return 1 === number ? 0 : 0 === number || number % 100 > 1 && number % 100 < 11 ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;
case "lv": return 0 === number ? 0 : 1 === number % 10 && 11 !== number % 100 ? 1 : 2;
case "pl": return 1 === number ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;
case "cy": return 1 === number ? 0 : 2 === number ? 1 : 8 === number || 11 === number ? 2 : 3;
case "ro": return 1 === number ? 0 : 0 === number || number % 100 > 0 && number % 100 < 20 ? 1 : 2;
case "ar": return 0 === number ? 0 : 1 === number ? 1 : 2 === number ? 2 : number % 100 >= 3 && number % 100 <= 10 ? 3 : number % 100 >= 11 && number % 100 <= 99 ? 4 : 5;
default: return 0;
}
}
// src/formatters/intl-formatter.ts
import { IntlMessageFormat } from "intl-messageformat";
function formatIntl(id, parameters, locale) {
if (id === "") {
return "";
}
const intlMessage = new IntlMessageFormat(id, [locale.replace("_", "-")], void 0, { ignoreTag: true });
parameters = { ...parameters };
Object.entries(parameters).forEach(([key, value]) => {
if (key.includes("%") || key.includes("{")) {
delete parameters[key];
parameters[key.replace(/[%{} ]/g, "").trim()] = value;
}
});
return intlMessage.format(parameters);
if (id === "") return "";
const intlMessage = new IntlMessageFormat(id, [locale.replace("_", "-")], void 0, { ignoreTag: true });
parameters = { ...parameters };
Object.entries(parameters).forEach(([key, value]) => {
if (key.includes("%") || key.includes("{")) {
delete parameters[key];
parameters[key.replace(/[%{} ]/g, "").trim()] = value;
}
});
return intlMessage.format(parameters);
}
// src/translator_controller.ts
function getDefaultLocale() {
return document.documentElement.getAttribute("data-symfony-ux-translator-locale") || // <html data-symfony-ux-translator-locale="en_US">
(document.documentElement.lang ? document.documentElement.lang.replace("-", "_") : null) || // <html lang="en-US">
"en";
return document.documentElement.getAttribute("data-symfony-ux-translator-locale") || (document.documentElement.lang ? document.documentElement.lang.replace("-", "_") : null) || "en";
}
function createTranslator({
messages,
locale = getDefaultLocale(),
localeFallbacks = {},
throwWhenNotFound = false
}) {
const _messages = messages;
const _localeFallbacks = localeFallbacks;
let _locale = locale;
let _throwWhenNotFound = throwWhenNotFound;
function setLocale(locale2) {
_locale = locale2;
}
function getLocale() {
return _locale;
}
function setThrowWhenNotFound(throwWhenNotFound2) {
_throwWhenNotFound = throwWhenNotFound2;
}
function trans(id, parameters = {}, domain = "messages", locale2 = null) {
if (typeof domain === "undefined") {
domain = "messages";
}
if (typeof locale2 === "undefined" || null === locale2) {
locale2 = _locale;
}
const message = _messages[id] ?? null;
if (message === null) {
return id;
}
const translationsIntl = message.translations[`${domain}+intl-icu`] ?? void 0;
if (typeof translationsIntl !== "undefined") {
while (typeof translationsIntl[locale2] === "undefined") {
locale2 = _localeFallbacks[locale2];
if (!locale2) {
break;
}
}
if (locale2) {
return formatIntl(translationsIntl[locale2], parameters, locale2);
}
}
const translations = message.translations[domain] ?? void 0;
if (typeof translations !== "undefined") {
while (typeof translations[locale2] === "undefined") {
locale2 = _localeFallbacks[locale2];
if (!locale2) {
break;
}
}
if (locale2) {
return format(translations[locale2], parameters, locale2);
}
}
if (_throwWhenNotFound) {
throw new Error(`No translation message found with id "${id}".`);
}
return id;
}
return {
setLocale,
getLocale,
setThrowWhenNotFound,
trans
};
function createTranslator({ messages, locale = getDefaultLocale(), localeFallbacks = {}, throwWhenNotFound = false }) {
const _messages = messages;
const _localeFallbacks = localeFallbacks;
let _locale = locale;
let _throwWhenNotFound = throwWhenNotFound;
function setLocale(locale) {
_locale = locale;
}
function getLocale() {
return _locale;
}
function setThrowWhenNotFound(throwWhenNotFound) {
_throwWhenNotFound = throwWhenNotFound;
}
function trans(id, parameters = {}, domain = "messages", locale = null) {
if (typeof domain === "undefined") domain = "messages";
if (typeof locale === "undefined" || null === locale) locale = _locale;
const message = _messages[id] ?? null;
if (message === null) return id;
const translationsIntl = message.translations[`${domain}+intl-icu`] ?? void 0;
if (typeof translationsIntl !== "undefined") {
while (typeof translationsIntl[locale] === "undefined") {
locale = _localeFallbacks[locale];
if (!locale) break;
}
if (locale) return formatIntl(translationsIntl[locale], parameters, locale);
}
const translations = message.translations[domain] ?? void 0;
if (typeof translations !== "undefined") {
while (typeof translations[locale] === "undefined") {
locale = _localeFallbacks[locale];
if (!locale) break;
}
if (locale) return format(translations[locale], parameters, locale);
}
if (_throwWhenNotFound) throw new Error(`No translation message found with id "${id}".`);
return id;
}
return {
setLocale,
getLocale,
setThrowWhenNotFound,
trans
};
}
export {
createTranslator,
getDefaultLocale
};
export { createTranslator, getDefaultLocale };

View File

@@ -11,7 +11,7 @@ import { format } from './formatters/formatter';
import { formatIntl } from './formatters/intl-formatter';
import type { DomainsOf, LocaleOf, LocaleType, MessageId, Messages, ParametersOf, RemoveIntlIcuSuffix } from './types';
export * from './types.d';
export type * from './types.d';
export function getDefaultLocale(): LocaleType {
return (