No vi ninguna mención en las respuestas existentes de cuestiones relacionadas con los puntos de código del plano astral o la internacionalización. "Mayúsculas" no significa lo mismo en todos los idiomas que utilizan un script determinado.
Inicialmente no vi ninguna respuesta que abordara los problemas relacionados con los puntos de código del plano astral. Hay uno , pero está un poco enterrado (¡como este, supongo!)
La mayoría de las funciones propuestas se ven así:
function capitalizeFirstLetter(str) {
return str[0].toUpperCase() + str.slice(1);
}
Sin embargo, algunos caracteres en mayúscula quedan fuera del BMP (plano multilingüe básico, puntos de código U + 0 a U + FFFF). Por ejemplo, tome este texto de Deseret:
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉"); // "𐐶𐐲𐑌𐐼𐐲𐑉"
El primer carácter aquí no puede capitalizarse porque las propiedades indexadas de la matriz de cadenas no acceden a "caracteres" o puntos de código *. Acceden a las unidades de código UTF-16. Esto también es cierto cuando se divide: los valores de índice apuntan a unidades de código.
Resulta que las unidades de código UTF-16 son 1: 1 con puntos de código USV dentro de dos rangos, U + 0 a U + D7FF y U + E000 a U + FFFF inclusive. La mayoría de los personajes en mayúsculas caen en esos dos rangos, pero no todos.
A partir de ES2015, lidiar con esto se volvió un poco más fácil. String.prototype[@@iterator]
produce cadenas correspondientes a puntos de código **. Entonces, por ejemplo, podemos hacer esto:
function capitalizeFirstLetter([ first, ...rest ]) {
return [ first.toUpperCase(), ...rest ].join('');
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
Para cadenas más largas, esto probablemente no sea terriblemente eficiente ***: realmente no necesitamos repetir el resto. Podríamos usar String.prototype.codePointAt
para llegar a esa primera (posible) letra, pero aún tendríamos que determinar dónde debería comenzar el corte. Una forma de evitar iterar el resto sería probar si el primer punto de código está fuera del BMP; si no es así, el corte comienza en 1, y si lo es, el corte comienza en 2.
function capitalizeFirstLetter(str) {
const firstCP = str.codePointAt(0);
const index = firstCP > 0xFFFF ? 2 : 1;
return String.fromCodePoint(firstCP).toUpperCase() + str.slice(index);
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
Podrías usar matemática bit a bit en lugar de > 0xFFFF
allí, pero probablemente sea más fácil de entender de esta manera y lograrías lo mismo.
También podemos hacer que esto funcione en ES5 y versiones posteriores llevando esa lógica un poco más lejos si es necesario. No hay métodos intrínsecos en ES5 para trabajar con puntos de código, por lo que debemos probar manualmente si la primera unidad de código es un sustituto ****:
function capitalizeFirstLetter(str) {
var firstCodeUnit = str[0];
if (firstCodeUnit < '\uD800' || firstCodeUnit > '\uDFFF') {
return str[0].toUpperCase() + str.slice(1);
}
return str.slice(0, 2).toUpperCase() + str.slice(2);
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
Al principio también mencioné consideraciones de internacionalización. Algunos de estos son muy difíciles de explicar porque requieren conocimiento no solo de qué idioma se está utilizando, sino que también pueden requerir un conocimiento específico de las palabras en el idioma. Por ejemplo, el dígrafo irlandés "mb" se capitaliza como "mB" al comienzo de una palabra. Otro ejemplo, el eszett alemán, nunca comienza una palabra (afaik), pero todavía ayuda a ilustrar el problema. La letra minúscula eszett ("ß") se escribe en mayúscula a "SS", pero "SS" podría minúscula a "ß" o "ss". ¡Se requiere conocimiento fuera de banda del idioma alemán para saber cuál es la correcta!
El ejemplo más famoso de este tipo de problemas, probablemente, es el turco. En latín turco, la forma mayúscula de i es İ, mientras que la forma minúscula de I es ı: son dos letras diferentes. Afortunadamente tenemos una forma de dar cuenta de esto:
function capitalizeFirstLetter([ first, ...rest ], locale) {
return [ first.toLocaleUpperCase(locale), ...rest ].join('');
}
capitalizeFirstLetter("italy", "en") // "Italy"
capitalizeFirstLetter("italya", "tr") // "İtalya"
En un navegador, la etiqueta de idioma más preferida del usuario se indica mediante navigator.language
, se encuentra una lista en orden de preferencia navigator.languages
, y se puede obtener el idioma de un elemento DOM dado (generalmente) con Object(element.closest('[lang]')).lang || YOUR_DEFAULT_HERE
documentos en varios idiomas .
En los agentes que admiten clases de caracteres de propiedad Unicode en RegExp, que se introdujeron en ES2018, podemos limpiar más aún expresando directamente en qué caracteres estamos interesados:
function capitalizeFirstLetter(str, locale=navigator.language) {
return str.replace(/^\p{CWU}/u, char => char.toLocaleUpperCase(locale));
}
Esto podría modificarse un poco para manejar también mayúsculas múltiples palabras en una cadena con bastante buena precisión. La propiedad de carácter CWU
o Changes_When_Uppercased coincide con todos los puntos de código que, bueno, cambian cuando se escribe en mayúscula. Podemos probar esto con caracteres de dígrafo enchapados en títulos como el holandés ij por ejemplo:
capitalizeFirstLetter('ijsselmeer'); // "IJsselmeer"
En el momento de escribir este artículo (febrero de 2020), Firefox / Spidermonkey aún no ha implementado ninguna de las características RegExp introducidas en los últimos dos años *****. Puede verificar el estado actual de esta función en la tabla de compatibilidad de Kangax . Babel puede compilar literales RegExp con referencias de propiedad a patrones equivalentes sin ellos, pero tenga en cuenta que el código resultante puede ser enorme.
Con toda probabilidad, las personas que hagan esta pregunta no se preocuparán por la capitalización o la internacionalización de Deseret. Pero es bueno estar al tanto de estos problemas porque hay una buena posibilidad de que los encuentres eventualmente, incluso si no son preocupaciones actualmente. No son casos "límite", o mejor dicho, no son casos límite por definición : hay un país entero donde la mayoría de la gente habla turco, de todos modos, y la combinación de unidades de código con puntos de código es una fuente bastante común de errores (especialmente con respecto a los emoji). ¡Tanto las cadenas como el lenguaje son bastante complicados!
* Las unidades de código de UTF-16 / UCS2 también son puntos de código Unicode en el sentido de que, por ejemplo, U + D800 es técnicamente un punto de código, pero eso no es lo que "significa" aquí ... más o menos ... aunque se pone bastante borroso. Sin embargo, lo que los sustitutos definitivamente no son son los USV (valores escalares Unicode).
** Sin embargo, si una unidad de código sustituto está "huérfana", es decir, no forma parte de un par lógico, también podría obtener sustitutos aquí.
*** tal vez. No lo he probado. A menos que haya determinado que la capitalización es un cuello de botella significativo, probablemente no me preocupe, elija lo que considere más claro y legible.
**** tal función podría desear probar las unidades de código primera y segunda en lugar de solo la primera, ya que es posible que la primera unidad sea un sustituto huérfano. Por ejemplo, la entrada "\ uD800x" capitalizaría la X tal cual, lo que puede esperarse o no.
***** Aquí está el problema de Bugzilla si quieres seguir el progreso más directamente.