Uppercasing does not guarantee the same length or a reversible transformation. Lookup keys, highlight offsets and stored text need explicit rules beyond a call to toUpperCase().
1. One character can expand into several
const original = '\u00DF'; // ß
const upper = original.toUpperCase();
console.log(original.length); // 1
console.log(upper); // SS
console.log(upper.length); // 2
console.log(upper.toLowerCase() === original); // false
Here ß becomes SS, and lowercasing that result produces ss rather than the original. toUpperCase returns a new string; see MDN String.toUpperCase. Uppercasing followed by lowercasing is not a lossless round-trip test.
Measure length at the stage required by the product. If the limit applies after conversion, validate the output using the agreed unit. Original offsets may no longer identify the same position in transformed text; highlighting needs a separate position-mapping strategy.
2. Locale can change lowercase results
console.log('I'.toLowerCase()); // i
console.log('I'.toLocaleLowerCase('tr') === '\u0131'); // true
const dotted = '\u0130'; // İ
console.log(dotted.toLocaleLowerCase('en-US').length); // 2
console.log(dotted.toLocaleLowerCase('tr')); // i
Turkish rules map I to dotless ı. For İ, the en-US result in this example occupies two UTF-16 code units, while tr produces i. MDN toLocaleLowerCase documents locale-aware mappings.
Choose locale according to the field's requirements. Persistent keys should not change with each user's interface language. Frontend and backend implementations need an agreed algorithm, locale and test corpus rather than merely similar function names.
3. ASCII codes can use a narrower contract
function canonicalCode(value) {
if (typeof value !== 'string') {
throw new TypeError('code must be a string');
}
if (!/^[A-Za-z0-9-]{1,24}$/.test(value)) {
throw new RangeError('invalid ASCII code');
}
return value.toLowerCase();
}
console.log(canonicalCode('HT-Ab12')); // ht-ab12
console.log(canonicalCode('HT-Ab12') === canonicalCode('ht-ab12')); // true
This helper accepts 1–24 ASCII Latin letters, digits or hyphens before lowercasing. It does not trim or coerce numbers and rejects non-ASCII text before transformation. Every allowed character maps one-to-one or remains unchanged, so this contract preserves length.
Case variants intentionally share a key. Check uniqueness on that key instead of silently overwriting records. A lone hyphen remains valid under this regex, so it is not a complete identifier format for every business. Names and multilingual text need different policies.
4. Separate original text from lookup keys
Keep the original when exact display matters and derive keys under a separate policy. Case conversion does not replace Unicode normalization, language-sensitive comparison or a full case-folding algorithm. Define what happens when multiple inputs produce the same key.
Before changing an existing policy, inspect stored data for collisions. A small code change can affect lookup relationships and unique constraints when its output changes.
5. Review before canonicalizing case
- Is this display text or a lookup key?
- Must the original remain available?
- Is length measured before or after conversion, and in which unit?
- Is locale explicit and consistent across systems?
- How are collisions between different inputs handled?
All six bilingual blocks ran with Node.js, with additional helper length, invalid-type and non-ASCII checks. Data migrations and real highlighting interfaces were not tested.




No comments yet. Be the first to share your thoughts.