Nie widziałem żadnej wzmianki w istniejących odpowiedziach na problemy związane z punktami kodu samolotu astralnego lub internacjonalizacją. „Wielkie litery” nie oznaczają tego samego w każdym języku używającym danego skryptu.
Początkowo nie widziałem żadnych odpowiedzi dotyczących problemów związanych z punktami kodu płaszczyzny astralnej. Jest jeden , ale jest trochę pochowany (tak chyba będzie!)
Większość proponowanych funkcji wygląda następująco:
function capitalizeFirstLetter(str) {
return str[0].toUpperCase() + str.slice(1);
}
Jednak niektóre znaki z literami nie mieszczą się w BMP (podstawowa płaszczyzna wielojęzyczna, punkty kodowe U + 0 do U + FFFF). Weźmy na przykład następujący tekst:
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉"); // "𐐶𐐲𐑌𐐼𐐲𐑉"
Pierwszy znak tutaj nie używa wielkich liter, ponieważ indeksowane przez tablicę właściwości ciągów nie mają dostępu do „znaków” ani punktów kodowych *. Dostęp do jednostek kodu UTF-16. Dotyczy to również krojenia - wartości indeksu wskazują jednostki kodu.
Zdarza się, że jednostki kodu UTF-16 to 1: 1 z punktami kodowymi USV w dwóch zakresach, od U + 0 do U + D7FF i U + E000 do U + FFFF włącznie. Większość znaków w skrzynkach należy do tych dwóch zakresów, ale nie wszystkie.
Począwszy od ES2015, radzenie sobie z tym stało się trochę łatwiejsze. String.prototype[@@iterator]
zwraca ciągi odpowiadające punktom kodowym **. Na przykład możemy to zrobić:
function capitalizeFirstLetter([ first, ...rest ]) {
return [ first.toUpperCase(), ...rest ].join('');
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
W przypadku dłuższych ciągów prawdopodobnie nie jest to strasznie wydajne *** - tak naprawdę nie musimy powtarzać reszty. Moglibyśmy użyć String.prototype.codePointAt
tej pierwszej (możliwej) litery, ale nadal musielibyśmy ustalić, od czego powinien zacząć się segment. Jednym ze sposobów uniknięcia iteracji reszty byłoby sprawdzenie, czy pierwszy punkt kodowy znajduje się poza BMP; jeśli nie jest, wycinek zaczyna się od 1, a jeśli tak, wycinek zaczyna się od 2.
function capitalizeFirstLetter(str) {
const firstCP = str.codePointAt(0);
const index = firstCP > 0xFFFF ? 2 : 1;
return String.fromCodePoint(firstCP).toUpperCase() + str.slice(index);
}
capitalizeFirstLetter("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
Zamiast tego możesz użyć matematyki bitowej > 0xFFFF
, ale prawdopodobnie łatwiej jest to zrozumieć i osiągnąć to samo.
Możemy również sprawić, aby działało to w ES5 i niższych, jeśli zajdzie taka potrzeba, posuwając się dalej. W ES5 nie ma żadnych wewnętrznych metod pracy z punktami kodowymi, dlatego musimy ręcznie przetestować, czy pierwsza jednostka kodu jest odpowiednikiem ****:
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("𐐶𐐲𐑌𐐼𐐲𐑉") // "𐐎𐐲𐑌𐐼𐐲𐑉"
Na początku wspomniałem również o kwestiach dotyczących internacjonalizacji. Niektóre z nich są bardzo trudne do wyjaśnienia, ponieważ wymagają wiedzy nie tylko o tym, jakiego języka używa się, ale mogą również wymagać konkretnej znajomości słów w tym języku. Na przykład irlandzki znak „mb” zaczyna się od wielkiej litery jako „mB” na początku słowa. Kolejny przykład, niemiecki eszett, nigdy nie zaczyna słowa (afaik), ale nadal pomaga zilustrować problem. Małe litery eszett („ß”) są wielkimi literami na „SS”, ale „SS” może składać się z małych liter na „ß” lub „ss” - potrzebujesz znajomości języka niemieckiego poza pasmem, aby wiedzieć, który jest prawidłowy!
Najbardziej znanym przykładem tego rodzaju problemów jest prawdopodobnie turecki. W łacinie tureckiej główną formą i jest İ, natomiast mała litera I to ı - są to dwie różne litery. Na szczęście mamy na to sposób:
function capitalizeFirstLetter([ first, ...rest ], locale) {
return [ first.toLocaleUpperCase(locale), ...rest ].join('');
}
capitalizeFirstLetter("italy", "en") // "Italy"
capitalizeFirstLetter("italya", "tr") // "İtalya"
W przeglądarce najbardziej preferowany znacznik językowy użytkownika jest oznaczony przez navigator.language
, lista w kolejności preferencji znajduje się w navigator.languages
, a język danego elementu DOM można uzyskać (zwykle) za pomocą Object(element.closest('[lang]')).lang || YOUR_DEFAULT_HERE
dokumentów wielojęzycznych.
W agentach, które obsługują klasy znaków właściwości Unicode w RegExp, które zostały wprowadzone w ES2018, możemy dalej posprzątać, bezpośrednio wyrażając, którymi znakami jesteśmy zainteresowani:
function capitalizeFirstLetter(str, locale=navigator.language) {
return str.replace(/^\p{CWU}/u, char => char.toLocaleUpperCase(locale));
}
Można to nieco ulepszyć, aby obsługiwać wielkie litery w ciągu z dość dobrą dokładnością. Właściwość znakowaCWU
lub Changes_When_Uppercased pasuje do wszystkich punktów kodowych, które zmieniają się, gdy są pisane wielkimi literami. Możemy spróbować się z titlecased znaków digraf jak holenderskiego ij na przykład:
capitalizeFirstLetter('ijsselmeer'); // "IJsselmeer"
W momencie pisania (luty 2020) Firefox / Spidermonkey nie wdrożył jeszcze żadnej funkcji RegExp wprowadzonej w ciągu ostatnich dwóch lat *****. Możesz sprawdzić aktualny status tej funkcji w tabeli kompatybilności Kangax . Babel jest w stanie skompilować literały RegExp z odwołaniami do właściwości do równoważnych wzorców bez nich, ale należy pamiętać, że wynikowy kod może być ogromny.
Najprawdopodobniej osoby zadające to pytanie nie będą zajmować się pisaniem wielkich liter ani internacjonalizacją. Ale dobrze jest zdawać sobie sprawę z tych problemów, ponieważ istnieje duża szansa, że w końcu się z nimi spotkasz, nawet jeśli obecnie nie są to obawy. Nie są to przypadki „krawędziowe”, a raczej nie są to przypadki z definicji z definicji - istnieje cały kraj, w którym większość ludzi mówi po turecku, a łączenie jednostek kodowych z punktami kodowymi jest dość powszechnym źródłem błędów (szczególnie z w odniesieniu do emoji). Zarówno łańcuchy, jak i język są dość skomplikowane!
* Jednostki kodowe UTF-16 / UCS2 są również punktami kodowymi Unicode w tym sensie, że np. U + D800 jest technicznie punktem kodowym, ale to nie jest to, co tutaj „znaczy”… tak jakby… chociaż robi się ładnie zamazany. Tym, czym zdecydowanie nie są surogaty, są jednak USV (wartości skalarne Unicode).
** Chociaż jeśli jednostka kodu zastępczego jest „osierocona” - tj. Nie jest częścią logicznej pary - nadal możesz uzyskać tutaj również zastępcze jednostki.
*** może. Nie testowałem tego. Jeśli nie określiłeś, że wielkie litery są znaczącym wąskim gardłem, prawdopodobnie nie przejmowałbym się tym - wybierz wszystko, co Twoim zdaniem jest najbardziej jasne i czytelne.
**** taka funkcja może chcieć przetestować zarówno pierwszą, jak i drugą jednostkę kodu zamiast tylko pierwszej, ponieważ możliwe jest, że pierwsza jednostka jest osieroconym surogatem. Na przykład dane wejściowe „\ uD800x” pisałyby wielką literą X w niezmienionej postaci, czego można oczekiwać lub nie.
***** Oto problem Bugzilli, jeśli chcesz śledzić postępy bardziej bezpośrednio.