Skip to content

Commit 002d748

Browse files
committed
Fix LocaleDisplay crash on odd-length generic BCP 47 extension subtags
1 parent b5e1a86 commit 002d748

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1414

1515
* `Localize.Locale.Provider.locale_cache_dir/0` validates its configuration at app start (via `Localize.Supervisor`) and raises `Localize.LocaleCacheDirError` instead of silently reading from the wrong directory at runtime.
1616

17+
* `Localize.Locale.LocaleDisplay.display_name/2` no longer raises `FunctionClauseError` on a BCP 47 generic extension carrying an odd number of subtags (e.g. `cr-s-7b`); the trailing singleton chunk now renders as a bare subtag.
18+
1719
### Enhancements
1820

1921
* New `:otp_app` config key. Three supported forms for the locale cache directory: (1) `:otp_app` only → `Application.app_dir(<otp_app>, "priv/localize/locales")`; (2) `:otp_app` + relative `:locale_cache_dir``Application.app_dir(<otp_app>, <relative>)`; (3) absolute `:locale_cache_dir` → used verbatim, `:otp_app` ignored.

lib/localize/locale/locale_display/extension.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,21 @@ defmodule Localize.Locale.LocaleDisplay.Extension do
3030
Localize.Substitution.substitute(["x", value_names], key_type_pattern)
3131

3232
{extension, key_value_pairs} when is_list(key_value_pairs) ->
33+
# Generic BCP 47 extensions (singletons other than `u` and `t`)
34+
# carry one or more 2–8 alphanumeric subtags. CLDR's locale
35+
# display pattern pairs them as key/type, but an odd-length
36+
# subtag list is well-formed BCP 47 — render the trailing
37+
# singleton as a bare subtag rather than crashing. (Property
38+
# regression: `Cr-S-7B` parses to extension `s` with value
39+
# `["7b"]`, which chunked to `[["7b"]]` did not match the
40+
# 2-element pattern.)
3341
value_names =
3442
key_value_pairs
3543
|> Enum.chunk_every(2)
36-
|> Enum.map(fn [key, value] -> "#{key}-#{value}" end)
44+
|> Enum.map(fn
45+
[key, value] -> "#{key}-#{value}"
46+
[value] -> "#{value}"
47+
end)
3748
|> join_field_values(display_names)
3849

3950
Localize.Substitution.substitute([extension, value_names], key_type_pattern)

test/localize/locale/locale_display_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,24 @@ defmodule Localize.Locale.LocaleDisplayTest do
177177
result = Localize.Locale.LocaleDisplay.display_name("xyz-invalid-totally-bad")
178178
assert match?({:error, _}, result)
179179
end
180+
181+
# Regression: a generic BCP 47 extension with an odd number of
182+
# subtags (e.g. `cr-s-7b` — extension singleton `s` carrying a
183+
# single subtag `7b`) crashed in
184+
# `Localize.Locale.LocaleDisplay.Extension.display_name/3` because
185+
# `Enum.chunk_every(2)` produced a singleton chunk that did not
186+
# match the `[key, value]` pattern. Caught by the
187+
# `LocaleDisplay.display_name/2 adversarial` property — atom input
188+
# `:Cr_S_7B` normalised to `cr-s-7b`.
189+
test "odd-length generic extension subtag list renders without crashing" do
190+
assert {:ok, name} = Localize.Locale.LocaleDisplay.display_name("cr-s-7b")
191+
assert String.contains?(name, "Cree")
192+
assert String.contains?(name, "7b")
193+
end
194+
195+
test "atom form of the same input does not raise" do
196+
assert {:ok, _name} = Localize.Locale.LocaleDisplay.display_name(:Cr_S_7B)
197+
end
180198
end
181199

182200
# Regression: `find_exemplar_city/2` (in `LocaleDisplay.U`) and

0 commit comments

Comments
 (0)