Text Handling intermediate text translate

Case-Insensitive Matching

XPath 1.0 has no lower-case() function. Learn the translate() idiom that gives you case-insensitive text and attribute matching in every browser.

Case-Insensitive Matching

Product copy changes case all the time: Sign in becomes Sign In, then SIGN IN when marketing gets a new style guide. XPath 1.0 comparisons are case-sensitive and there is no lower-case() function. The tool you have is translate().

How translate() works

translate(string, from, to) replaces each character found in from with the character at the same position in to:

translate('Hello', 'HELLO', 'hello')    returns "hello"
translate('Hello', 'lo', 'LO')          returns "HeLLO"

If to is shorter than from, the unmatched characters are deleted:

translate('$1,200', '$,', '')           returns "1200"

The lower-case idiom

Map the whole uppercase alphabet to lowercase, then compare against a lowercase literal:

//button[translate(normalize-space(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='sign in']

It is verbose, so most teams wrap it in a helper in their test code:

static String lower(String expr) {
  return "translate(" + expr + ", 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')";
}
String xpath = "//button[" + lower("normalize-space()") + "='sign in']";
UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
LOWER = UPPER.lower()

def lower(expr: str) -> str:
    return f"translate({expr}, '{UPPER}', '{LOWER}')"

xpath = f"//button[{lower('normalize-space()')}='sign in']"

Case-insensitive contains

Same idea with contains():

//a[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'sign')]

Case-insensitive attributes

Attribute values are case-sensitive too. Frameworks sometimes emit type="Submit" or Type="submit" in generated markup. Attribute names are lowercased by the HTML parser, so @Type never matches in HTML, but values must be handled:

//input[translate(@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')='submit']

Non-ASCII letters

The idiom only folds the characters you list. For German, French or Turkish content, extend both strings:

translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜÉÈÀ', 'abcdefghijklmnopqrstuvwxyzäöüéèà')

This is a real limitation. If your app is heavily localised, prefer attribute-based locators and leave text to the assertion layer, where your language has proper Unicode case folding.

When not to bother

If the case of a label is part of the requirement, a case-sensitive match is a feature, not a bug: it catches an unintended change. Reserve case-insensitive locators for elements where the case genuinely is not meaningful, or where you cannot control the source.

What XPath 2.0+ offers instead

lower-case(), upper-case(), and matches(str, pattern, 'i') all exist from XPath 2.0 onward, and are available in XSLT 2.0 processors, Saxon, BaseX and some XML tools. They are not available in browsers, Selenium, Playwright, Appium or Cypress. See the XPath versions guide.

Try It Yourself

Open in Playground →

Next Steps

  1. XPath 1.0 Functions Reference - Everything translate() can be combined with
  2. Dynamic Attributes - Other tricks for markup you do not control
  3. XPath in Selenium - Where helper functions like lower() belong in a framework