Skip to content

Understanding Quality Values

Cyril Kato edited this page Jan 20, 2026 · 1 revision

Table of Contents

Understanding Quality Values

Quality values (or q-values) express the user's relative preference for each language. They are the key mechanism browsers use to communicate how much a user wants each language, not just which languages they accept.

What is a Quality Value?

A quality value is a decimal number between 0 and 1 that indicates preference:

  • q=1 — Maximum preference (I really want this language)
  • q=0.5 — Medium preference (This is acceptable)
  • q=0 — Not acceptable (I don't want this language)
In the header, quality values are specified with the ;q= suffix:
Accept-Language: en;q=1, fr;q=0.8, de;q=0.5

Default Quality Value

When no quality value is specified, the default is 1 (maximum preference):

Accept-Language: en, fr;q=0.8

This is equivalent to:

Accept-Language: en;q=1, fr;q=0.8
# Both produce the same result
AcceptLanguage.parse("en, fr;q=0.8").match(:en, :fr)
# => :en

AcceptLanguage.parse("en;q=1, fr;q=0.8").match(:en, :fr)
# => :en

How Quality Values Affect Matching

The gem selects the available language with the highest quality value:

parser = AcceptLanguage.parse("fr;q=0.7, en;q=0.9, de;q=0.8")

parser.match(:en, :fr, :de)
# => :en (q=0.9 is the highest)

parser.match(:fr, :de)
# => :de (q=0.8 beats q=0.7)

parser.match(:fr)
# => :fr (only option available)

Visual Representation

Think of quality values as a ranked preference list:

Header: "da, en-GB;q=0.8, en;q=0.7, *;q=0.5"

Priority ranking:
┌─────────────┬─────────┬──────────────────────┐
│ Rank        │ Q-Value │ Language             │
├─────────────┼─────────┼──────────────────────┤
│ 1st choice  │ 1.0     │ da (Danish)          │
│ 2nd choice  │ 0.8     │ en-GB (UK English)   │
│ 3rd choice  │ 0.7     │ en (English)         │
│ Last resort │ 0.5     │ * (anything else)    │
└─────────────┴─────────┴──────────────────────┘

Valid Quality Value Syntax

Per RFC 7231 Section 5.3.1, quality values must:

  • Be between 0 and 1
  • Have at most 3 decimal places
Value Valid? Notes
1 Maximum preference
0 Not acceptable
0.8 One decimal place
0.85 Two decimal places
0.123 Three decimal places (maximum)
1.0 Equivalent to 1
1.000 Equivalent to 1
0.1234 Too many decimal places
1.5 Greater than 1
-0.5 Negative value
.5 Missing leading zero

Invalid quality values are silently ignored, and the associated language is skipped:

# "en" has invalid q-value (> 1), so it's ignored
AcceptLanguage.parse("en;q=1.5, fr;q=0.8").match(:en, :fr)
# => :fr

Tie-Breaking: Declaration Order

When two languages have the same quality value, the one declared first in the header wins:

# Same q-value, different order
AcceptLanguage.parse("en;q=0.8, fr;q=0.8").match(:en, :fr)
# => :en (declared first)

AcceptLanguage.parse("fr;q=0.8, en;q=0.8").match(:en, :fr)
# => :fr (declared first)

This ensures deterministic results: the same header always produces the same match.

The Special Case: q=0

A quality value of 0 means the language is explicitly not acceptable. This is different from simply not listing the language:

# English is explicitly excluded
AcceptLanguage.parse("*, en;q=0").match(:en, :fr)
# => :fr

# Even with wildcard, en is rejected
AcceptLanguage.parse("*, en;q=0").match(:en)
# => nil

See Excluding Languages for more details on this powerful feature.

Practical Examples

Browser Settings to Header

A user configures their browser with these preferences:

  1. French (France) — primary
  2. French — secondary
  3. English — fallback
The browser sends:
Accept-Language: fr-FR, fr;q=0.9, en;q=0.8

Matching Against Your App

Your app supports :en, :fr, and :de:

header = "fr-FR, fr;q=0.9, en;q=0.8"
AcceptLanguage.parse(header).match(:en, :fr, :de)
# => :fr

The user wanted fr-FR (q=1), but since you don't have it, the gem falls back to fr (q=0.9).

Granular Preferences

Some users have very specific preferences:

header = "de-CH;q=1, de-AT;q=0.9, de-DE;q=0.8, de;q=0.7, en;q=0.5"

# Your app supports German variants
AcceptLanguage.parse(header).match(:de, :"de-DE", :"de-AT")
# => :"de-AT" (q=0.9, since de-CH is unavailable)

What's Next?

Clone this wiki locally