Repository: sindresorhus/camelcase (js)

Issue:
Correctly handle Unicode characters
The regular expressions in this package don’t support unicode characters such as `àçéèù` so `camelCase("Licorne époustouflante")` outputs `licorne époustouflante`.

Perhaps we could add a `transliterate` option that leverages [transliterate](https://www.npmjs.com/package/@sindresorhus/transliterate) so `camelCase("Licorne époustouflante", { transliterate: true })` outputs `licorneEpoustouflante`.

Another slightly more complicated alternative would be to have `camelCase("Licorne époustouflante", { transliterate: true })` output `licorneÉpoustouflante`.

What do you think? I could put a PR together.

Pull request:
Handle Unicode character casing and require Node.js 10
## what & why

Adds handling of non-ASCII character casing.

After merging this PR a string like `розовый_пушистый-единороги` will be converted to `розовыйПушистыйЕдинороги` (or to `РозовыйПушистыйЕдинороги` when pascal case is on) and `Licorne époustouflante` to `licorneÉpoustouflante` (/ `LicorneÉpoustouflante`).

Fixes #60 

## choices
Explanation of choices that might seem non-obvious:
- Replaces the `\w` with `[\p{Alpha}\p{N}_]` which is as close to the original intent of `\w` as I could muster (which is `[A-Za-z0-9_]` according to [mdn](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes))
- Uses _unicode property escapes_ in most of the regular expressions as suggested in #60. This implies use of node >=10. I've updated the package.json and travis config to reflect this (+ added node _stable_ to the build matrix).
- Keeps most of the existing logic in tact (if it ain't broken ...) except for this pattern in the preserveCamelCase function:

```javascript
isLastCharLower && /[a-zA-Z]/.test(character) && character.toUpperCase() === character
```

```javascript
isLastCharLower && /[\p{Lu}]/u.test(character)
```

Because the regular expression already implies it's an upper case character the `character.toUpperCase() === character` would always yield `true` anyway (but holler if I missed something!)

Patch:
diff --git a/.travis.yml b/.travis.yml
index e155464..94ab01f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,5 +2,3 @@ language: node_js
 node_js:
   - '12'
   - '10'
-  - '8'
-  - '6'
diff --git a/index.d.ts b/index.d.ts
index 58f2069..0d4aeed 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -13,6 +13,8 @@ declare const camelcase: {
 	/**
 	Convert a dash/dot/underscore/space separated string to camelCase or PascalCase: `foo-bar` → `fooBar`.
 
+	Correctly handles Unicode strings.
+
 	@param input - String to convert to camel case.
 
 	@example
@@ -27,6 +29,9 @@ declare const camelcase: {
 
 	camelCase('Foo-Bar');
 	//=> 'fooBar'
+	
+	camelCase('розовый_пушистый_единороги');
+	//=> 'розовыйПушистыйЕдинороги'
 
 	camelCase('Foo-Bar', {pascalCase: true});
 	//=> 'FooBar'
diff --git a/index.js b/index.js
index 579f99b..dd2c490 100644
--- a/index.js
+++ b/index.js
@@ -8,21 +8,21 @@ const preserveCamelCase = string => {
 	for (let i = 0; i < string.length; i++) {
 		const character = string[i];
 
-		if (isLastCharLower && /[a-zA-Z]/.test(character) && character.toUpperCase() === character) {
+		if (isLastCharLower && /[\p{Lu}]/u.test(character)) {
 			string = string.slice(0, i) + '-' + string.slice(i);
 			isLastCharLower = false;
 			isLastLastCharUpper = isLastCharUpper;
 			isLastCharUpper = true;
 			i++;
-		} else if (isLastCharUpper && isLastLastCharUpper && /[a-zA-Z]/.test(character) && character.toLowerCase() === character) {
+		} else if (isLastCharUpper && isLastLastCharUpper && /[\p{Ll}]/u.test(character)) {
 			string = string.slice(0, i - 1) + '-' + string.slice(i - 1);
 			isLastLastCharUpper = isLastCharUpper;
 			isLastCharUpper = false;
 			isLastCharLower = true;
 		} else {
-			isLastCharLower = character.toLowerCase() === character && character.toUpperCase() !== character;
+			isLastCharLower = character.toLocaleLowerCase() === character && character.toLocaleUpperCase() !== character;
 			isLastLastCharUpper = isLastCharUpper;
-			isLastCharUpper = character.toUpperCase() === character && character.toLowerCase() !== character;
+			isLastCharUpper = character.toLocaleUpperCase() === character && character.toLocaleLowerCase() !== character;
 		}
 	}
 
@@ -34,11 +34,12 @@ const camelCase = (input, options) => {
 		throw new TypeError('Expected the input to be `string | string[]`');
 	}
 
-	options = Object.assign({
-		pascalCase: false
-	}, options);
+	options = {
+		...{pascalCase: false},
+		...options
+	};
 
-	const postProcess = x => options.pascalCase ? x.charAt(0).toUpperCase() + x.slice(1) : x;
+	const postProcess = x => options.pascalCase ? x.charAt(0).toLocaleUpperCase() + x.slice(1) : x;
 
 	if (Array.isArray(input)) {
 		input = input.map(x => x.trim())
@@ -53,10 +54,10 @@ const camelCase = (input, options) => {
 	}
 
 	if (input.length === 1) {
-		return options.pascalCase ? input.toUpperCase() : input.toLowerCase();
+		return options.pascalCase ? input.toLocaleUpperCase() : input.toLocaleLowerCase();
 	}
 
-	const hasUpperCase = input !== input.toLowerCase();
+	const hasUpperCase = input !== input.toLocaleLowerCase();
 
 	if (hasUpperCase) {
 		input = preserveCamelCase(input);
@@ -64,9 +65,9 @@ const camelCase = (input, options) => {
 
 	input = input
 		.replace(/^[_.\- ]+/, '')
-		.toLowerCase()
-		.replace(/[_.\- ]+(\w|$)/g, (_, p1) => p1.toUpperCase())
-		.replace(/\d+(\w|$)/g, m => m.toUpperCase());
+		.toLocaleLowerCase()
+		.replace(/[_.\- ]+([\p{Alpha}\p{N}_]|$)/gu, (_, p1) => p1.toLocaleUpperCase())
+		.replace(/\d+([\p{Alpha}\p{N}_]|$)/gu, m => m.toLocaleUpperCase());
 
 	return postProcess(input);
 };
diff --git a/package.json b/package.json
index fbdbaaa..028c2e2 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
 		"url": "sindresorhus.com"
 	},
 	"engines": {
-		"node": ">=6"
+		"node": ">=10"
 	},
 	"scripts": {
 		"test": "xo && ava && tsd"
diff --git a/readme.md b/readme.md
index 5476a1c..38eda30 100644
--- a/readme.md
+++ b/readme.md
@@ -2,6 +2,8 @@
 
 > Convert a dash/dot/underscore/space separated string to camelCase or PascalCase: `foo-bar` → `fooBar`
 
+Correctly handles Unicode strings.
+
 
 ## Install
 
@@ -24,6 +26,9 @@ camelCase('foo_bar');
 camelCase('Foo-Bar');
 //=> 'fooBar'
 
+camelCase('розовый_пушистый_единороги');
+//=> 'розовыйПушистыйЕдинороги'
+
 camelCase('Foo-Bar', {pascalCase: true});
 //=> 'FooBar'
 


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index ed6c78e..cc565d9 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -2,6 +2,7 @@ import {expectType} from 'tsd';
 import camelCase = require('.');
 
 expectType<string>(camelCase('foo-bar'));
+expectType<string>(camelCase('розовый_пушистый_единороги'));
 expectType<string>(camelCase('Foo-Bar', {pascalCase: true}));
 expectType<string>(camelCase(['foo', 'bar']));
 expectType<string>(camelCase(['__foo__', '--bar'], {pascalCase: true}));
diff --git a/test.js b/test.js
index 7ca00e7..4dea4be 100644
--- a/test.js
+++ b/test.js
@@ -58,6 +58,12 @@ test('camelCase', t => {
 	t.is(camelCase('1Hello'), '1Hello');
 	t.is(camelCase('1hello'), '1Hello');
 	t.is(camelCase('h2w'), 'h2W');
+	t.is(camelCase('розовый_пушистый-единороги'), 'розовыйПушистыйЕдинороги');
+	t.is(camelCase('розовый_пушистый-единороги'), 'розовыйПушистыйЕдинороги');
+	t.is(camelCase('РОЗОВЫЙ_ПУШИСТЫЙ-ЕДИНОРОГИ'), 'розовыйПушистыйЕдинороги');
+	t.is(camelCase('桑德在这里。'), '桑德在这里。');
+	t.is(camelCase('桑德在这里。'), '桑德在这里。');
+	t.is(camelCase('桑德_在这里。'), '桑德在这里。');
 });
 
 test('camelCase with pascalCase option', t => {
@@ -117,6 +123,11 @@ test('camelCase with pascalCase option', t => {
 	t.is(camelCase('1hello', {pascalCase: true}), '1Hello');
 	t.is(camelCase('1Hello', {pascalCase: true}), '1Hello');
 	t.is(camelCase('h1W', {pascalCase: true}), 'H1W');
+	t.is(camelCase('РозовыйПушистыйЕдинороги', {pascalCase: true}), 'РозовыйПушистыйЕдинороги');
+	t.is(camelCase('розовый_пушистый-единороги', {pascalCase: true}), 'РозовыйПушистыйЕдинороги');
+	t.is(camelCase('РОЗОВЫЙ_ПУШИСТЫЙ-ЕДИНОРОГИ', {pascalCase: true}), 'РозовыйПушистыйЕдинороги');
+	t.is(camelCase('桑德在这里。', {pascalCase: true}), '桑德在这里。');
+	t.is(camelCase('桑德_在这里。', {pascalCase: true}), '桑德在这里。');
 });
 
 test('invalid input', t => {