@johnmorrisdotca/taisen-brackets 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ All notable changes to this project are written here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-10-14
10
+
11
+ ### Added
12
+
13
+ - **MLS**: `playoffFormat("mls", year)` for the format since 2023 (18 teams, nine a conference; a wild card match, a best-of-three round one, then single matches to the MLS Cup; seven teams of each conference skip the wild card match), checked against English Wikipedia, with the round names in all five languages. A demo example, the 2024 MLS Cup Playoffs, read from spxn.com's MLS data (openfootball, CC0, checked against English Wikipedia, CC BY-SA 4.0), with a test that pins its champion (LA Galaxy) and final (2–1) and a picture.
14
+ - `LayoutBox.byes`: every side that skipped the earlier rounds, which includes both sides of a later-round tie that no tie feeds (MLS's round one); each such side says "bye" beside its score.
15
+
16
+ ### Fixed
17
+
18
+ - A series counts a game level at the end of play as won by the side that won the shoot-out (MLS's best-of-three round one), so its score and winner are right.
19
+
20
+ ## [0.2.0] - 2026-10-14
21
+
22
+ ### Added
23
+
24
+ - **Languages.** A `locale` option: `"en"` (the default), `"fr"`, `"ja"`, `"es"`, `"de"`, or a `Locale` made with `defineLocale`. Every text the SVG and the table write is in a table of words per language: the round names (the football terms of each language's Wikipedia article on the World Cup; the leagues' rounds as generic words, with a league's own name kept as the language uses it), the notes (after extra time, 4–2 on penalties, replay, aggregate, bye, walkover), the champion's label, the table's headers, the tooltips and the accessible description, and the numbers and dates through `Intl`. The `<svg>` and the `<table>` carry `lang`. `words` still replaces any string. Exported: `LOCALES`, `LOCALE_CODES`, `defineLocale`, `localeOf`, `voiceOf`, `localizedLayout`, `noteLong`, and the types `Locale` and `Voice`. The Spanish and German words were checked against Wikipedia only and need a native reader (docs/languages.md).
25
+ - **Team names in a language.** `Team.names` (by locale code), the `teamName(team, locale)` option, and `countryName(isoCode, locale)`, which asks `Intl.DisplayNames` (the home nations too), so a World Cup shows every country in the chosen language with no list written. `Team.region` carries the ISO code.
26
+ - A tie can be a `walkover`; a side that had a bye says so beside its score in the language.
27
+ - **A real example for every sport**, read from spxn.com like the others: the 2024 NBA playoffs, the playoffs of the 2024 NFL season (byes, the Super Bowl at the centre), the 2024 CFL playoffs (byes, the Grey Cup), and the 1984 Stanley Cup playoffs (the Campbell and Wales conferences, a best-of-five first round), beside the NHL 2024, MLB 2022 and the World Cups. Each has its source in NOTICE.md and the demo's footer, and a test that pins its champion and final score.
28
+ - **The demo** has a sport picker with its editions, a language switcher (EN, FR, 日本語, ES, DE) that drives every example, the poster included, remembered in `localStorage`, and the shared family header and footer (the family template of 2026-10-15b, `scripts/page.mjs`, the family block of the README).
29
+ - docs/languages.md: every term of every language, what it was checked against, and what still needs a native reader. The README has a "Languages" section and a gallery of the sports.
30
+
31
+ ### Changed
32
+
33
+ - The year of a format is stated: the playoffs' year, except for the NFL, whose year is its season's.
34
+ - The champion's German label is "Turniersieger"; the football terms in Japanese, French, Spanish and German were reviewed (French and Japanese by native-language reviewers).
35
+
9
36
  ## [0.1.1] - 2026-10-14
10
37
 
11
38
  - The title is now "Taisen Brackets 対戦" in the README, the demo and the package description, matching the package name.
@@ -25,6 +52,8 @@ First version.
25
52
  - A demo with real results read from spxn.com (the NHL 2024 playoffs, the World Cups of 2022, 1938 and 2026, the MLB 2022 postseason; sources and terms in NOTICE.md), two made-up examples (two-legged ties, a 32-team field) and a flags-only poster of the 2026 World Cup, in a playground that changes the size, look and shape.
26
53
  - Playoff formats checked against English Wikipedia, each era citing its articles (docs/formats.md). The year is the year the playoffs are played in.
27
54
 
28
- [Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.1...HEAD
55
+ [Unreleased]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.1...HEAD
56
+ [0.2.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.2.0...v0.2.1
57
+ [0.2.0]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.1...v0.2.0
29
58
  [0.1.1]: https://github.com/johnmorrisdotca/taisen-brackets/compare/v0.1.0...v0.1.1
30
59
  [0.1.0]: https://github.com/johnmorrisdotca/taisen-brackets/releases/tag/v0.1.0
package/NOTICE.md CHANGED
@@ -7,11 +7,17 @@ The code of this package is under the MIT licence (see [LICENSE](LICENSE)). The
7
7
  | `playoffFormat`: round names, series lengths, byes and teams by era | Facts (years, round names, series lengths, numbers of teams, byes) read on 2026-10-13 from English Wikipedia's articles on each playoff and tournament; the articles are cited era by era in [docs/formats.md](docs/formats.md). The MediaWiki API was read one request at a time, a second or more apart, with a User-Agent naming this project | Wikipedia's text is CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/). Only facts are taken, no sentence or table, and the articles are credited in docs/formats.md |
8
8
  | The demo's World Cup results of 1938 and 2022 (`demo/data/worldcup-1938-2022.js`) | The Fjelstul World Cup Database, https://github.com/jfjelstul/worldcup, © 2023 Joshua C. Fjelstul, Ph.D., read through the public API of [spxn.com](https://spxn.com), which holds it as the source of those matches | **CC BY-SA 4.0** (https://creativecommons.org/licenses/by-sa/4.0/legalcode). That file is a selection of the database's fields and is shared under the same licence, with this credit. The pictures in `docs/images` that draw these results (the World Cup 2022 and 1938 brackets, and the phone pictures) are adaptations of it and carry the same licence and credit. The package, which has no data in it, is MIT |
9
9
  | The demo's World Cup results of 2026 (`demo/data/worldcup-2026.js`) | openfootball/worldcup, https://github.com/openfootball/worldcup, read through spxn.com | CC0 |
10
- | The demo's NHL results (`demo/data/nhl-2024.js`) | Every game of the 2024 Stanley Cup playoffs and each team's conference, from the NHL ("NHL", as spxn.com credits it), read through spxn.com | Facts; credited to the NHL |
10
+ | The demo's NHL results (`demo/data/nhl-2024.js`, `demo/data/nhl-1984.js`) | Every game of the 2024 and the 1984 Stanley Cup playoffs and each team's conference, from the NHL ("NHL", as spxn.com credits it), read through spxn.com | Facts; credited to the NHL |
11
+ | The demo's NBA results (`demo/data/nba-2024.js`) | Every game of the 2024 NBA playoffs and each team's conference, from English Wikipedia (https://en.wikipedia.org/wiki/2024_NBA_playoffs), read through spxn.com, which holds them as sourced from Wikipedia | **CC BY-SA 4.0** (https://creativecommons.org/licenses/by-sa/4.0/legalcode). That file is an adaptation and is shared under the same licence, with this credit; the pictures of the NBA bracket carry it too |
12
+ | The demo's MLS results (`demo/data/mls-2024.js`) | Every match of the 2024 MLS Cup Playoffs and each club's conference, read through spxn.com, whose MLS data is from openfootball/world (CC0, https://github.com/openfootball/world) checked against English Wikipedia (https://en.wikipedia.org/wiki/2024_MLS_Cup_Playoffs) | **CC BY-SA 4.0** (Wikipedia is in the mix, so the file is an adaptation shared under the same licence, with this credit); openfootball's part is CC0; the pictures of the MLS bracket carry the same licence |
13
+ | The demo's NFL results (`demo/data/nfl-2024.js`) | Every game of the playoffs of the 2024 NFL season (January and February 2025) and each team's conference, from nflverse (https://github.com/nflverse/nflverse-data), read through spxn.com | CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/), credited to nflverse |
14
+ | The demo's CFL results (`demo/data/cfl-2024.js`) | Every game of the 2024 CFL playoffs and each team's division, from English Wikipedia (https://en.wikipedia.org/wiki/2024_CFL_season), read through spxn.com | **CC BY-SA 4.0**, as for the NBA file above; the pictures of the CFL bracket carry it too |
11
15
  | The demo's MLB results (`demo/data/mlb-2022.js`) | Every game of the 2022 MLB postseason, read through spxn.com. The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at www.retrosheet.org | Retrosheet's notice, kept as it asks |
12
16
  | How the results were read | spxn.com's public API (a PostgREST service whose `api` schema is readable with a publishable key; the key is not kept in this repository), by `scripts/fetch-demo-data.mjs`, one request at a time, a second apart, retrieved 2026-10-14 | spxn.com is the same owner's site |
13
17
  | The demo's two-legged ties and its 32-team generic bracket | Made up for the demo: the clubs do not exist and the teams are "Team 1" to "Team 32" | Not real |
14
18
  | The demo's national flags (in `demo/flags-data.js`) | [Hata](https://github.com/johnmorrisdotca/hata) 1.5.0 (MIT code), a development dependency used only by `scripts/make-flags.mjs`. Each flag is a Wikimedia Commons file that Hata ships only when Commons states it public domain, CC0 or CC BY; the file, author and licence of each are in `FLAG_CREDITS` in that file and in Hata's manifest and NOTICE | Each flag's own licence; none of the flags used asks for a credit. Commons marks flags as official insignia, whose use some countries restrict independently of copyright |
19
+ | The words of the languages (`src/locales.ts`) | The football terms (the rounds of the World Cup, extra time, the shoot-out, a replay, a walkover) were read on 2026-10-14 in the French, Spanish, German and Japanese Wikipedia articles on the World Cups of 2022, 2026 and 1938; the articles are cited in [docs/languages.md](docs/languages.md). The other words are written for this package; the French and Japanese were read by native-language reviewers, the Spanish and German were not | Wikipedia's text is CC BY-SA 4.0; only terms (single words and short phrases) are taken, no sentence or table, and the articles are credited in docs/languages.md |
20
+ | The names of countries in each language | Not in the package: `countryName` asks `Intl.DisplayNames`, which holds Unicode CLDR's names; the demo passes it each team's ISO code. The home nations and the two states that no longer exist are written in `src/locales.ts` and `demo/data.js` | CLDR is under the Unicode licence (https://www.unicode.org/license.txt) |
15
21
  | The demo's page | Written for this package, with the family stylesheet `demo/family.css` | MIT |
16
22
 
17
23
  The flags in the demo are for the nations of the World Cups of 2022 and 2026: `AR AT AU BA BE BR CA CD CH CI CO CV DE DZ EC EG ES FR GB-ENG GH HR JP KR MA MX NL NO PL PT PY SE SN US ZA`. The package itself ships no flag, no logo and no data of the leagues.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">Taisen Brackets <sub>対戦</sub></h1>
2
2
 
3
3
  <p align="center"><strong>Knockout brackets as SVG, from a list of ties.</strong><br>
4
- Give it the games, series, two-legged ties or replays of a tournament and it works out the rounds, the halves, the byes and who feeds whom, then draws the bracket: two halves that meet at the final, or one tree left to right, wide, square or tall for a phone, in light, dark or a gold-on-navy poster look, with each team's logo or round flag, the champion in the centre, and a table for readers the drawing does not serve. NHL, NBA, MLB, NFL, CFL and World Cup formats by era. One function, no dependencies.</p>
4
+ Give it the games, series, two-legged ties or replays of a tournament and it works out the rounds, the halves, the byes and who feeds whom, then draws the bracket: two halves that meet at the final, or one tree left to right, wide, square or tall for a phone, in light, dark or a gold-on-navy poster look, with each team's logo or round flag, the champion in the centre, and a table for readers the drawing does not serve. NHL, NBA, MLB, NFL, CFL, MLS and World Cup formats by era, in English, French, Japanese, Spanish and German. One function, no dependencies.</p>
5
5
 
6
6
  <p align="center">
7
7
  <a href="https://github.com/johnmorrisdotca/taisen-brackets/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/johnmorrisdotca/taisen-brackets/actions/workflows/ci.yml/badge.svg"></a>
@@ -31,6 +31,33 @@ Give it the games, series, two-legged ties or replays of a tournament and it wor
31
31
  </tr>
32
32
  </table>
33
33
 
34
+ ## Every sport
35
+
36
+ One layout draws them all, each from real results: the demo has a picker for the sport and, under it, the editions. [docs/formats.md](./docs/formats.md) has the rounds by era.
37
+
38
+ <table>
39
+ <tr>
40
+ <td align="center" valign="top"><img alt="The 2024 NHL playoffs: Western and Eastern conferences, best-of-seven series, Florida Panthers as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/nhl-desk-light.png" width="360"><br><em>NHL 2024, series</em></td>
41
+ <td align="center" valign="top"><img alt="The 1984 Stanley Cup playoffs: the Campbell and Wales conferences, a best-of-five first round, Edmonton Oilers as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/nhl1984-desk-light.png" width="360"><br><em>NHL 1984, an older era</em></td>
42
+ <td align="center" valign="top"><img alt="The 2024 NBA playoffs: sixteen teams in two conferences, Boston Celtics as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/nba-desk-light.png" width="360"><br><em>NBA 2024</em></td>
43
+ </tr>
44
+ <tr>
45
+ <td align="center" valign="top"><img alt="The 2022 MLB postseason: byes for the top two seeds of each league, Houston Astros as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/byes-desk-light.png" width="360"><br><em>MLB 2022, byes</em></td>
46
+ <td align="center" valign="top"><img alt="The playoffs of the 2024 NFL season: single games, byes for the top seed of each conference, the Super Bowl at the centre, Philadelphia Eagles as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/nfl-desk-light.png" width="360"><br><em>NFL 2024 season, the Super Bowl</em></td>
47
+ <td align="center" valign="top"><img alt="The 2024 CFL playoffs: division semi-finals and finals, byes, the Grey Cup at the centre, Toronto Argonauts as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/cfl-desk-light.png" width="360"><br><em>CFL 2024, the Grey Cup</em></td>
48
+ </tr>
49
+ <tr>
50
+ <td align="center" valign="top"><img alt="The 2022 World Cup knockout stage." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/hero-desk-light.png" width="360"><br><em>World Cup 2022</em></td>
51
+ <td align="center" valign="top"><img alt="The 1938 World Cup: replays." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/replays-desk-light.png" width="360"><br><em>World Cup 1938, replays</em></td>
52
+ <td align="center" valign="top"><img alt="The 2026 World Cup as a flags-only poster, in Japanese." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/poster-desk-ja.png" width="360"><br><em>World Cup 2026 poster, in Japanese</em></td>
53
+ </tr>
54
+ <tr>
55
+ <td align="center" valign="top"><img alt="The 2024 MLS Cup Playoffs: a wild card match, a best-of-three round one, then single matches to the MLS Cup, LA Galaxy as champion." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/mls-desk-light.png" width="360"><br><em>MLS 2024, best of three</em></td>
56
+ <td></td>
57
+ <td></td>
58
+ </tr>
59
+ </table>
60
+
34
61
  ## In 30 seconds
35
62
 
36
63
  ```sh
@@ -68,7 +95,8 @@ Put the string in a page (`element.innerHTML = svg`), write it to a file, or sen
68
95
  - **Team marks you supply**: a logo address or inline SVG, drawn round or square; a team with none is its initials on a colour. Round flags from [Hata](https://github.com/johnmorrisdotca/hata) go in as they are.
69
96
  - **The champion in the centre**, with a slot for your trophy.
70
97
  - **Accessible**: `role="img"` with a title and a description, a tooltip sentence on every tie, and `bracketTable`.
71
- - **Formats by era**: NHL, NBA, MLB, NFL, CFL and FIFA World Cup round names, series lengths and byes.
98
+ - **Formats by era**: NHL, NBA, MLB, NFL, CFL, MLS and FIFA World Cup round names, series lengths and byes.
99
+ - **Five languages**: English, French, Japanese, Spanish and German, for every word the drawing, the tooltips, the description and the table write, with the football terms of each language; countries named by `Intl` from their ISO code; numbers and dates by `Intl`. Add a language of your own.
72
100
  - **Deterministic**: no clock, no random number, no counter; the same input gives the same string, and ids come from a hash of the input.
73
101
  - **No dependencies**, ESM with types, about 40 kB packed.
74
102
 
@@ -274,7 +302,7 @@ console.log(bracketTable(bracket).startsWith("<table")); // true
274
302
 
275
303
  ### Your own words
276
304
 
277
- Every word the package writes is in `WORDS`; pass `words` to replace any: `{ champion: "Vainqueur", thirdPlace: "Match pour la 3e place", tbd: "À déterminer" }`. Round names come from `roundNames`: a list, or a format from `playoffFormat`.
305
+ Every word the package writes is in `WORDS` (English; see Languages for the others); pass `words` to replace any: `{ champion: "Vainqueur", thirdPlace: "Match pour la 3e place", tbd: "À déterminer" }`. Round names come from `roundNames`: a list, or a format from `playoffFormat`.
278
306
 
279
307
  ### In React, Vue and the rest
280
308
 
@@ -291,18 +319,56 @@ The string is built from your data, with every name escaped; do not put untruste
291
319
 
292
320
  Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part from `/layout`, the formats from `/presets`. The types are in the package; each function's doc comment has its details.
293
321
 
294
- - `drawBracket(input, options?)`: the SVG string. `input` is a list of matchups, `{ matchups, teams }` or a layout. Options: `size`, `orientation`, `shape`, `detail`, `theme`, `background`, `title`, `subtitle`, `roundNames`, `roundLabels`, `scores`, `seeds`, `markShape`, `champion`, `trophy`, `thirdPlace`, `winnerHighlight`, `words`, `groupOrder`, `id`, `className`.
322
+ - `drawBracket(input, options?)`: the SVG string. `input` is a list of matchups, `{ matchups, teams }` or a layout. Options: `size`, `orientation`, `shape`, `detail`, `theme`, `background`, `title`, `subtitle`, `roundNames`, `roundLabels`, `scores`, `seeds`, `markShape`, `champion`, `trophy`, `thirdPlace`, `winnerHighlight`, `locale`, `words`, `teamName`, `groupOrder`, `id`, `className`.
295
323
  - `layoutBracket(input, options?)`: the rounds, halves, rows, byes and results, without drawing. `shape: "ltr"` and `groupOrder` are its options.
296
324
  - `bracketTable(input, options?)`: the HTML table.
297
325
  - `describeBracket(input, options?)`: the sentence a screen reader gets.
298
326
  - `singleElimination(teams, options?)`, `advanceWinners(matchups)`, `standardSeeds(size)`: brackets for any number of teams.
299
327
  - `matchupsFromGames(games, options)`: matchups from a flat list of games.
300
- - `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`: one tie's result, the teams of a bracket, a tie in a sentence.
301
- - `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `worldcup`).
328
+ - `resolveMatchup`, `collectTeams`, `describeTie`, `noteParts`, `noteLong`: one tie's result, the teams of a bracket, a tie in a sentence.
329
+ - `playoffFormat(league, year)`, `playoffEras(league)`, `LEAGUES`: the formats by era (`nhl`, `nba`, `mlb`, `nfl`, `cfl`, `mls`, `worldcup`).
302
330
  - `roundName(remaining)`, `roundNamesFor(count)`, `roundLabel(layout, round, names, words)`: round names.
303
- - `SIZES`, `THEMES`, `WORDS`, `VERSION`: the sizes, the looks' colours, the words, the version.
331
+ - `LOCALES`, `LOCALE_CODES`, `defineLocale`, `localeOf`, `voiceOf`, `countryName`: the languages (see Languages), a language of your own, the formats and the names of countries from `Intl`. `localizedLayout(layout, voice)` is a layout with the team names in a language.
332
+ - `SIZES`, `THEMES`, `WORDS`, `VERSION`: the sizes, the looks' colours, the English words, the version.
304
333
  - `cleanSvg(markup)`, `safeUrl(url)`, `abbreviate(name)`: the rules for marks, and the short name made from a name.
305
334
 
335
+ ## Languages
336
+
337
+ A World Cup is read in many languages, so every text the package writes (the round names, the notes, the champion's label, the table's headers, the accessible description, the numbers and the dates) has a table of words in English, French, Japanese, Spanish and German. Pass `locale`:
338
+
339
+ ```js
340
+ import { countryName, describeBracket } from "@johnmorrisdotca/taisen-brackets";
341
+
342
+ const teams = [{ id: "ARG", name: "Argentina", region: "AR" }, { id: "FRA", name: "France", region: "FR" }];
343
+ const matchups = [{ sides: ["ARG", "FRA"], score: [3, 3], extraTime: true, pens: [4, 2], date: "2022-12-18" }];
344
+ // A country's name in the language comes from Intl, by its ISO code: no list of names to write.
345
+ const teamName = (team, locale) => team.region && countryName(team.region, locale);
346
+
347
+ for (const locale of ["en", "fr", "ja", "es", "de"]) console.log(describeBracket({ matchups, teams }, { locale, teamName }));
348
+ // 2 teams, 1 round. Final: Argentina 3, France 3 (after extra time, 4–2 on penalties). Winner: Argentina. Champion: Argentina.
349
+ // 2 équipes, 1 tour. Finale : Argentine 3, France 3 (après prolongation, 4–2 aux tirs au but). Vainqueur : Argentine. Champion : Argentine.
350
+ // 2チーム、1ラウンド。決勝:アルゼンチン 3–3 フランス(延長戦、PK戦 4–2)。勝者:アルゼンチン。優勝:アルゼンチン。
351
+ // 2 equipos, 1 ronda. Final: Argentina 3, Francia 3 (tras la prórroga, 4–2 en los penales). Ganador: Argentina. Campeón: Argentina.
352
+ // 2 Teams, 1 Runde. Finale: Argentinien 3, Frankreich 3 (nach Verlängerung, 4:2 im Elfmeterschießen). Sieger: Argentinien. Turniersieger: Argentinien.
353
+ ```
354
+
355
+ <p align="center"><img alt="The same bracket, the World Cup of 2022 on a phone's layout, in English, French, Japanese, Spanish and German side by side: the rounds, the notes, the label under the champion and the names of the countries are each in their language." src="https://raw.githubusercontent.com/johnmorrisdotca/taisen-brackets/main/docs/images/languages-wc2022.png" width="900"></p>
356
+
357
+ - `locale` is `"en"` (the default), `"fr"`, `"ja"`, `"es"`, `"de"`, or a `Locale` you made with `defineLocale`. It sets the words, the names of the rounds, the `lang` of the `<svg>` and of the `<table>`, the digits and the dates (`Intl`), and capitals by the language's rules (not for Japanese). `LOCALES` holds the tables and `LOCALE_CODES` lists the codes.
358
+ - **Round names** are the football terms of each language's Wikipedia article on the World Cup: *Huitièmes de finale*, *Octavos de final*, *Achtelfinale*, ラウンド16; the leagues' rounds (NHL, NBA, MLB, NFL, CFL) are the generic words translated, and a league's own name is kept in the form the language uses (*Finale de la Coupe Stanley*, *Stanley-Cup-Finale*, *Coupe Grey*, *Super Bowl*, スーパーボウル). [docs/languages.md](./docs/languages.md) lists every term, what it was checked against, and what still needs a native reader (Spanish and German).
359
+ - **Team names**: a team can carry its names (`names: { fr: "Allemagne", ja: "ドイツ" }`), the option `teamName(team, locale)` can give one, and `countryName("DE", locale)` makes a country's name from `Intl.DisplayNames` (the home nations `GB-ENG`, `GB-SCT`, `GB-WLS` and `GB-NIR` too). A club keeps its name, and a team's short name (`NED`, `ARG`) stays the FIFA code in every language.
360
+ - **`words`** still replaces any string of any language: `{ locale: "fr", words: { champion: "Vainqueur" } }`.
361
+ - **A language of your own**: `defineLocale("nl", { words: { champion: "Kampioen" }, rounds: { "Quarter-finals": "Kwartfinales" } })` fills what you leave out from English; pass the result as `locale`.
362
+
363
+ ```js
364
+ import { defineLocale, drawBracket } from "@johnmorrisdotca/taisen-brackets";
365
+
366
+ const dutch = defineLocale("nl", { words: { champion: "Kampioen" }, rounds: { Final: "Finale" } });
367
+ const svg = drawBracket([{ sides: ["A", "B"], score: [2, 1] }], { locale: dutch });
368
+ console.log(svg.includes("KAMPIOEN")); // true
369
+ console.log(svg.includes('lang="nl"')); // true
370
+ ```
371
+
306
372
  ## Sports covered
307
373
 
308
374
  `playoffFormat(league, year)` gives the round names, the length of each series, the number of teams and the byes; [docs/formats.md](./docs/formats.md) lists the eras.
@@ -314,10 +380,11 @@ Everything is exported from `@johnmorrisdotca/taisen-brackets`; the layout part
314
380
  | MLB | 1969 on (no postseason in 1994) | Wild Card Series, Division Series, League Championship Series, World Series |
315
381
  | NFL | 1970 on, with 1982 as its own | Wild Card, Divisional, Conference Championship, Super Bowl |
316
382
  | CFL | 1996 on | Division Semi-Final, Division Final, Grey Cup |
383
+ | MLS | 2023 on | Wild Card Round, Round One (best of three), Conference Semifinals, Conference Finals, MLS Cup |
317
384
  | FIFA World Cup | every edition with a knockout, 1930 to 2026 (1950 had none) | Round of 32 (2026), Round of 16, Quarter-finals, Semi-finals, Final |
318
385
  | Any other | single elimination for any number of teams | Round of N … Final |
319
386
 
320
- Each era was checked against English Wikipedia (facts only; the articles are cited in [docs/formats.md](./docs/formats.md)). The year is the year the playoffs are played in: the NHL's 2024 is the 2023-24 season. A season did something of its own (a strike, a pandemic) has its own entry, and a later one is not covered until it is checked.
387
+ Each era was checked against English Wikipedia (facts only; the articles are cited in [docs/formats.md](./docs/formats.md)). The year is the year the playoffs are played in, except for the NFL, whose year is its season's, so 2024 is the season whose playoffs were played in January and February 2025. A season did something of its own (a strike, a pandemic) has its own entry, and a later one is not covered until it is checked.
321
388
 
322
389
  ## Theming
323
390
 
@@ -335,7 +402,7 @@ The names are `bg`, `ink`, `muted`, `line`, `box`, `edge`, `win`, `gold`, `plain
335
402
  - Text is measured by an estimate from character classes, not a font file, so that the string is the same on every machine; a long name is cut with an ellipsis a few percent early or late.
336
403
  - A series with no `bestOf` and no winner is not decided, whatever the games won say.
337
404
  - On a phone the champion is at the top and the third-place match at the foot; in a wide bracket of two halves they are in the centre.
338
- - Names, round names and the words are English; pass `words` and `roundNames` for another language. Japanese words are on the roadmap.
405
+ - The built-in languages are English, French, Japanese, Spanish and German; the Spanish and German words were checked against Wikipedia only and need a native reader (docs/languages.md). A club or a person is not translated, and a team's short name is the same in every language.
339
406
 
340
407
  ## Accessibility
341
408
 
@@ -348,7 +415,7 @@ Any current browser, and Node 22 or later. The package makes no network request
348
415
  ## Roadmap
349
416
 
350
417
  - Double elimination, and round-robin groups beside a knockout.
351
- - Japanese words.
418
+ - More languages (Portuguese, Italian, Korean, Chinese), and native review of the Spanish and German words.
352
419
  - A custom element and a React helper, as in the other packages of the family.
353
420
  - A PNG renderer (through an optional peer).
354
421
 
@@ -364,6 +431,42 @@ Libraries that draw brackets exist for several frameworks. Taisen's difference i
364
431
 
365
432
  The layout began as the playoff bracket of [spxn.com](https://spxn.com), the historical record of five leagues, where one layout draws a hockey playoff and a World Cup. [docs/migrating-from-spxn.md](./docs/migrating-from-spxn.md) is how that site would use the package.
366
433
 
434
+ ### The family
435
+
436
+ <!-- family:start (made by scripts/family-readme.mjs from scripts/family-template.mjs; change those, not this) -->
437
+ Taisen Brackets is one of twenty-six packages, each made for the same site, each at
438
+ [github.com/johnmorrisdotca](https://github.com/johnmorrisdotca). The code of every one is MIT.
439
+
440
+ - [Korokoro](https://github.com/johnmorrisdotca/korokoro) (コロコロ): dice, with notation, exact odds, real sounds and the dice of many games. [Demo](https://johnmorrisdotca.github.io/korokoro/).
441
+ - [Kyuubu](https://github.com/johnmorrisdotca/kyuubu) (キューブ): a turning cube for the browser, 2×2 to 7×7, with record solves to replay. [Demo](https://johnmorrisdotca.github.io/kyuubu/).
442
+ - [Hitotsu](https://github.com/johnmorrisdotca/hitotsu) (一つ): a colour-card shedding game for two to eight, with the house rules people play. [Demo](https://johnmorrisdotca.github.io/hitotsu/).
443
+ - [Toranpu](https://github.com/johnmorrisdotca/toranpu) (トランプ): a deck of playing cards, card games with computer players, and solitaires. [Demo](https://johnmorrisdotca.github.io/toranpu/).
444
+ - [Tane](https://github.com/johnmorrisdotca/tane) (種): seeded random numbers and daily seeds, the same in every browser and on every server. [Demo](https://johnmorrisdotca.github.io/tane/).
445
+ - [Narabe](https://github.com/johnmorrisdotca/narabe) (並べ): one rules engine for abstract board games, from gomoku and Reversi to Go and checkers. [Demo](https://johnmorrisdotca.github.io/narabe/).
446
+ - [Tenka](https://github.com/johnmorrisdotca/tenka) (天下): world conquest for two to six, on a map of the real world. [Demo](https://johnmorrisdotca.github.io/tenka/).
447
+ - [Kumimoji](https://github.com/johnmorrisdotca/kumimoji) (組み文字): a crossword tile race, in English and Japanese kana. [Demo](https://johnmorrisdotca.github.io/kumimoji/).
448
+ - [Tsunagi](https://github.com/johnmorrisdotca/tsunagi) (繋ぎ): a line-joining logic puzzle whose every level has exactly one answer. [Demo](https://johnmorrisdotca.github.io/tsunagi/).
449
+ - [Jarajara](https://github.com/johnmorrisdotca/jarajara) (ジャラジャラ): mahjong tiles drawn as SVG, stacked layouts, and the matching solitaire Awase. [Demo](https://johnmorrisdotca.github.io/jarajara/).
450
+ - [Suido](https://github.com/johnmorrisdotca/suido) (水道): a pipe puzzle: turn the pieces until the water reaches every drain. [Demo](https://johnmorrisdotca.github.io/suido/).
451
+ - [Domino](https://github.com/johnmorrisdotca/domino) (ドミノ): dominoes and Mexican Train. [Demo](https://johnmorrisdotca.github.io/domino/).
452
+ - [Kotoba](https://github.com/johnmorrisdotca/kotoba) (言葉): word lists and word-game rules in English, French, German and Japanese. [Demo](https://johnmorrisdotca.github.io/kotoba/).
453
+ - [Sugoroku](https://github.com/johnmorrisdotca/sugoroku) (双六): backgammon and its variants, with the doubling cube and match play. [Demo](https://johnmorrisdotca.github.io/sugoroku/).
454
+ - [Kazu](https://github.com/johnmorrisdotca/kazu) (数): grid number puzzles: Sudoku and its variants, Futoshiki and Skyscrapers. [Demo](https://johnmorrisdotca.github.io/kazu/).
455
+ - [Meikyuu](https://github.com/johnmorrisdotca/meikyuu) (迷宮): mazes on squares, hexagons, triangles and circles, made from a seed and drawn through with a finger or the mouse. [Demo](https://johnmorrisdotca.github.io/meikyuu/).
456
+ - [Hikidashi](https://github.com/johnmorrisdotca/hikidashi) (引き出し): a drawer of small Japanese text tools: era dates, kanji numerals, readings and sentence difficulty. [Demo](https://johnmorrisdotca.github.io/hikidashi/).
457
+ - [Chizu](https://github.com/johnmorrisdotca/chizu) (地図): maps of the world and of countries' regions, in English and Japanese, with a quiz and callouts. [Demo](https://johnmorrisdotca.github.io/chizu/).
458
+ - [Bushu](https://github.com/johnmorrisdotca/bushu) (部首): find a kanji by the parts it is made of. [Demo](https://johnmorrisdotca.github.io/bushu/).
459
+ - [Tobiishi](https://github.com/johnmorrisdotca/tobiishi) (飛び石): peg solitaire with nine boards and seeded solvable challenges. [Demo](https://johnmorrisdotca.github.io/tobiishi/).
460
+ - [Jirai](https://github.com/johnmorrisdotca/jirai) (地雷): minesweeper on shaped grids with verified no-guess boards. [Demo](https://johnmorrisdotca.github.io/jirai/).
461
+ - [Gunjin](https://github.com/johnmorrisdotca/gunjin) (軍人): five hidden-rank strategy games with pass-the-device play. [Demo](https://johnmorrisdotca.github.io/gunjin/).
462
+ - [Karakuri](https://github.com/johnmorrisdotca/karakuri) (からくり): eight hyper-casual puzzle games, some of them physics: draw a shield, pull pins, cut ropes, slide blocks, pour tubes. [Demo](https://johnmorrisdotca.github.io/karakuri/).
463
+ - [Houseki](https://github.com/johnmorrisdotca/houseki) (宝石): gem and stone matching puzzles: falling triplets, stone collapse, colour chains and gem swap. [Demo](https://johnmorrisdotca.github.io/houseki/).
464
+ - [Kyogijo Stadiums](https://github.com/johnmorrisdotca/kyogijo-stadiums) (競技場): sports fields, rinks, courts and ballparks drawn to scale as SVG, with each team and the score on them. [Demo](https://johnmorrisdotca.github.io/kyogijo-stadiums/).
465
+ - [Taisen Brackets](https://github.com/johnmorrisdotca/taisen-brackets) (対戦): a sport-agnostic tournament bracket, drawn as SVG, from a list of ties. [Demo](https://johnmorrisdotca.github.io/taisen-brackets/).
466
+
467
+ **This package is Taisen Brackets.** The demos of all twenty-six share one header and footer, so each links the rest.
468
+ <!-- family:end -->
469
+
367
470
  ## Development
368
471
 
369
472
  ```sh
package/dist/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  export * from "./layout-entry.ts";
2
- export { drawBracket, describeBracket, SIZES } from "./render.ts";
2
+ export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.ts";
3
3
  export { bracketTable } from "./table.ts";
4
4
  export type { TableOptions } from "./table.ts";
5
5
  export { THEMES } from "./theme.ts";
6
6
  export type { Palette } from "./theme.ts";
7
- export { WORDS } from "./words.ts";
8
7
  export { cleanSvg, safeUrl } from "./marks.ts";
9
8
  export { abbreviate } from "./text.ts";
10
9
  export { VERSION } from "./version.ts";
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  export * from "./layout-entry.js";
2
- export { drawBracket, describeBracket, SIZES } from "./render.js";
2
+ export { drawBracket, describeBracket, localizedLayout, SIZES } from "./render.js";
3
3
  export { bracketTable } from "./table.js";
4
4
  export { THEMES } from "./theme.js";
5
- export { WORDS } from "./words.js";
6
5
  export { cleanSvg, safeUrl } from "./marks.js";
7
6
  export { abbreviate } from "./text.js";
8
7
  export { VERSION } from "./version.js";
@@ -1,6 +1,8 @@
1
1
  export * from "./types.ts";
2
2
  export { layoutBracket } from "./layout.ts";
3
- export { collectTeams, resolveMatchup, describeTie, noteParts } from "./matchup.ts";
3
+ export { collectTeams, resolveMatchup, describeTie, noteParts, noteLong } from "./matchup.ts";
4
4
  export { roundName, roundNamesFor, roundLabel } from "./rounds.ts";
5
5
  export { standardSeeds, singleElimination, advanceWinners, matchupsFromGames } from "./seeding.ts";
6
6
  export type { SingleEliminationOptions, PlayedGame, GamesOptions } from "./seeding.ts";
7
+ export { WORDS, LOCALES, LOCALE_CODES, defineLocale, localeOf, voiceOf, countryName } from "./locales.ts";
8
+ export type { Voice } from "./locales.ts";
@@ -1,5 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export { layoutBracket } from "./layout.js";
3
- export { collectTeams, resolveMatchup, describeTie, noteParts } from "./matchup.js";
3
+ export { collectTeams, resolveMatchup, describeTie, noteParts, noteLong } from "./matchup.js";
4
4
  export { roundName, roundNamesFor, roundLabel } from "./rounds.js";
5
5
  export { standardSeeds, singleElimination, advanceWinners, matchupsFromGames } from "./seeding.js";
6
+ export { WORDS, LOCALES, LOCALE_CODES, defineLocale, localeOf, voiceOf, countryName } from "./locales.js";
package/dist/layout.js CHANGED
@@ -140,6 +140,16 @@ export function layoutBracket(input, options = {}) {
140
140
  const other = (1 - fed);
141
141
  return ties[i].sides[other] ? other : null;
142
142
  };
143
+ /** Every side that skipped the rounds before this tie's: the bye side, or both sides of a later-round tie that nothing feeds. */
144
+ const byesOf = (i) => {
145
+ const one = byeOf(i);
146
+ if (one !== null)
147
+ return [one];
148
+ const ch = children.get(i) ?? [];
149
+ if (round[i] <= 1 || ch.length > 0)
150
+ return [];
151
+ return [0, 1].filter((k) => ties[i].sides[k] !== null);
152
+ };
143
153
  /** Lays one tree out: leaves take the next rows, a tie sits at the middle of the ones that feed it. */
144
154
  const lay = (root, mark) => {
145
155
  const boxes = [];
@@ -148,7 +158,7 @@ export function layoutBracket(input, options = {}) {
148
158
  const ch = children.get(i) ?? [];
149
159
  const ys = ch.map(go);
150
160
  const y = ys.length ? ys.reduce((a, b) => a + b, 0) / ys.length : next++;
151
- boxes.push({ i, round: round[i], y, to: null, fed: ch.length > 0, order: orderOf(i), bye: byeOf(i) });
161
+ boxes.push({ i, round: round[i], y, to: null, fed: ch.length > 0, order: orderOf(i), bye: byeOf(i), byes: byesOf(i) });
152
162
  place[i] = mark;
153
163
  return y;
154
164
  };
@@ -0,0 +1,42 @@
1
+ import type { DrawOptions, Locale, Team, Words } from "./types.ts";
2
+ /** The built-in languages, by code. */
3
+ export declare const LOCALES: Readonly<Record<"en" | "fr" | "ja" | "es" | "de", Locale>>;
4
+ /** The codes of the built-in languages, English first. */
5
+ export declare const LOCALE_CODES: readonly string[];
6
+ /** The English words, as they were before there were languages. */
7
+ export declare const WORDS: Readonly<Words>;
8
+ /**
9
+ * A language of your own: what you give, on top of the base language's (English unless `base` says another) for what you leave out.
10
+ * `tag` is what `Intl` formats numbers and dates with (default `code`).
11
+ */
12
+ export declare function defineLocale(code: string, parts: {
13
+ tag?: string;
14
+ words?: Partial<Words>;
15
+ rounds?: Readonly<Record<string, string>>;
16
+ }, base?: keyof typeof LOCALES): Locale;
17
+ /** The language an option names: a code of a built-in one, or a `Locale`. */
18
+ export declare function localeOf(option: string | Locale | undefined): Locale;
19
+ /** A language ready to write with: the words with any replacements, and the formats. */
20
+ export interface Voice {
21
+ locale: Locale;
22
+ words: Words;
23
+ /** A number in the language's own digits and separators. */
24
+ number(n: number): string;
25
+ /** A date as the language writes it ("Dec 18, 2022", "2022/12/18"), or `null` when `iso` does not start with a date. */
26
+ date(iso: string): string | null;
27
+ /** Capitals, by the language's own rules (not for Japanese). */
28
+ upper(text: string): string;
29
+ /** A round's name in the language. */
30
+ round(name: string): string;
31
+ /** A team's name in the language. */
32
+ team(team: Team): string;
33
+ }
34
+ export declare function voiceOf(options: Pick<DrawOptions, "locale" | "words" | "teamName">): Voice;
35
+ /** Fills `{n}`-style holes in a phrase. */
36
+ export declare function fill(phrase: string, values: Record<string, string | number>): string;
37
+ /**
38
+ * The name of a country in a language, from its ISO 3166 code, through `Intl.DisplayNames`: no list to write ("DE" is
39
+ * Germany, Allemagne, ドイツ, Alemania, Deutschland). The home nations (`GB-ENG`, `GB-SCT`, `GB-WLS`, `GB-NIR`) are not regions
40
+ * to `Intl`, so they have names here. A code `Intl` does not know gives `undefined`.
41
+ */
42
+ export declare function countryName(code: string, locale?: string | Locale): string | undefined;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The languages: English, French, Japanese, Spanish and German. A language is its words (every text a drawing and a table write),
3
+ * the names of the rounds by their English names, and the tag that `Intl` formats numbers and dates for. Football terms (the
4
+ * rounds of the World Cup, extra time, the shoot-out) were checked against each language's Wikipedia article on the 2022 World Cup
5
+ * (docs/languages.md); the other leagues' round names are the generic words, and a league's own proper name (the Stanley Cup, the
6
+ * Grey Cup, the Super Bowl) is kept in its conventional form for the language.
7
+ */
8
+ const EN = {
9
+ tbd: "TBD", bye: "bye", walkover: "walkover", champion: "Champion", thirdPlace: "Third place", other: "Other ties",
10
+ aet: "a.e.t.", pens: "pens", aggregate: "agg.", replay: "replay",
11
+ aetLong: "after extra time", pensScore: "{a}–{b} on penalties", aggregateLong: "on aggregate",
12
+ inGames: "in {n}", bestOf: "best of {n}", round: "Round {n}", roundOf: "Round of {n}",
13
+ tieVs: "{a} v {b}", tieScore: "{a} {sa}, {b} {sb}", scoreText: "{a}–{b}", winnerIs: "Winner: {name}", championIs: "Champion: {name}", finalLine: "{round}: {tie}",
14
+ summary: "{n} teams, {r} rounds", summaryOne: "{n} teams, 1 round", sep: ". ", end: ".", note: " ({notes})", noteJoin: ", ",
15
+ columnRound: "Round", columnTie: "Tie", columnNote: "Note", columnDate: "Date", title: "Knockout bracket",
16
+ };
17
+ const FR = {
18
+ tbd: "À déterminer", bye: "exempt", walkover: "forfait", champion: "Champion", thirdPlace: "Match pour la troisième place", other: "Autres rencontres",
19
+ aet: "a.p.", pens: "t.a.b.", aggregate: "cumulé", replay: "match rejoué",
20
+ aetLong: "après prolongation", pensScore: "{a}–{b} aux tirs au but", aggregateLong: "au cumul",
21
+ inGames: "en {n}", bestOf: "au meilleur des {n}", round: "Tour {n}", roundOf: "Tour à {n} équipes",
22
+ tieVs: "{a} contre {b}", tieScore: "{a} {sa}, {b} {sb}", scoreText: "{a}–{b}", winnerIs: "Vainqueur : {name}", championIs: "Champion : {name}", finalLine: "{round} : {tie}",
23
+ summary: "{n} équipes, {r} tours", summaryOne: "{n} équipes, 1 tour", sep: ". ", end: ".", note: " ({notes})", noteJoin: ", ",
24
+ columnRound: "Tour", columnTie: "Rencontre", columnNote: "Note", columnDate: "Date", title: "Tableau à élimination directe",
25
+ };
26
+ const JA = {
27
+ tbd: "未定", bye: "シード", walkover: "不戦勝", champion: "優勝", thirdPlace: "3位決定戦", other: "その他の対戦",
28
+ aet: "延長", pens: "PK戦", aggregate: "合計", replay: "再試合",
29
+ aetLong: "延長戦", pensScore: "PK戦 {a}–{b}", aggregateLong: "合計スコア",
30
+ inGames: "{n}戦", bestOf: "{n}戦{w}勝制", round: "第{n}ラウンド", roundOf: "ラウンド{n}",
31
+ tieVs: "{a} 対 {b}", tieScore: "{a} {sa}–{sb} {b}", scoreText: "{a}–{b}", winnerIs: "勝者:{name}", championIs: "優勝:{name}", finalLine: "{round}:{tie}",
32
+ summary: "{n}チーム、{r}ラウンド", summaryOne: "{n}チーム、1ラウンド", sep: "。", end: "。", note: "({notes})", noteJoin: "、",
33
+ columnRound: "ラウンド", columnTie: "対戦", columnNote: "備考", columnDate: "日付", title: "トーナメント表",
34
+ };
35
+ const ES = {
36
+ tbd: "Por definir", bye: "exento", walkover: "incomparecencia", champion: "Campeón", thirdPlace: "Partido por el tercer puesto", other: "Otros partidos",
37
+ aet: "prórroga", pens: "penales", aggregate: "global", replay: "desempate",
38
+ aetLong: "tras la prórroga", pensScore: "{a}–{b} en los penales", aggregateLong: "en el global",
39
+ inGames: "en {n}", bestOf: "al mejor de {n}", round: "Ronda {n}", roundOf: "Ronda de {n}",
40
+ tieVs: "{a} vs. {b}", tieScore: "{a} {sa}, {b} {sb}", scoreText: "{a}–{b}", winnerIs: "Ganador: {name}", championIs: "Campeón: {name}", finalLine: "{round}: {tie}",
41
+ summary: "{n} equipos, {r} rondas", summaryOne: "{n} equipos, 1 ronda", sep: ". ", end: ".", note: " ({notes})", noteJoin: ", ",
42
+ columnRound: "Ronda", columnTie: "Partido", columnNote: "Nota", columnDate: "Fecha", title: "Cuadro de eliminatorias",
43
+ };
44
+ const DE = {
45
+ tbd: "offen", bye: "Freilos", walkover: "kampflos", champion: "Turniersieger", thirdPlace: "Spiel um Platz 3", other: "Weitere Begegnungen",
46
+ aet: "n. V.", pens: "i. E.", aggregate: "ges.", replay: "Wiederholungsspiel",
47
+ aetLong: "nach Verlängerung", pensScore: "{a}:{b} im Elfmeterschießen", aggregateLong: "im Gesamtergebnis",
48
+ inGames: "in {n}", bestOf: "Best of {n}", round: "Runde {n}", roundOf: "Runde der letzten {n}",
49
+ tieVs: "{a} gegen {b}", tieScore: "{a} {sa}, {b} {sb}", scoreText: "{a}:{b}", winnerIs: "Sieger: {name}", championIs: "Turniersieger: {name}", finalLine: "{round}: {tie}",
50
+ summary: "{n} Teams, {r} Runden", summaryOne: "{n} Teams, 1 Runde", sep: ". ", end: ".", note: " ({notes})", noteJoin: ", ",
51
+ columnRound: "Runde", columnTie: "Begegnung", columnNote: "Hinweis", columnDate: "Datum", title: "Turnierbaum",
52
+ };
53
+ /**
54
+ * The round names, English name first. The football rounds are the terms of each language's Wikipedia article on the World Cup;
55
+ * the leagues' rounds are the generic words (first round, semi-finals), and the league's own name is kept as the language writes it.
56
+ */
57
+ const ROUNDS = {
58
+ "Round of 32": ["Seizièmes de finale", "ラウンド32", "Dieciseisavos de final", "Sechzehntelfinale"],
59
+ "Round of 16": ["Huitièmes de finale", "ラウンド16", "Octavos de final", "Achtelfinale"],
60
+ "Quarter-finals": ["Quarts de finale", "準々決勝", "Cuartos de final", "Viertelfinale"],
61
+ "Quarterfinals": ["Quarts de finale", "準々決勝", "Cuartos de final", "Viertelfinale"],
62
+ "Semi-finals": ["Demi-finales", "準決勝", "Semifinales", "Halbfinale"],
63
+ "Semifinals": ["Demi-finales", "準決勝", "Semifinales", "Halbfinale"],
64
+ "Final": ["Finale", "決勝", "Final", "Finale"],
65
+ "Preliminary Round": ["Tour préliminaire", "予選ラウンド", "Ronda preliminar", "Vorrunde"],
66
+ "Qualifying Round": ["Tour de qualification", "予選ラウンド", "Ronda de clasificación", "Qualifikationsrunde"],
67
+ "First Round": ["Premier tour", "1回戦", "Primera ronda", "Erste Runde"],
68
+ "Second Round": ["Deuxième tour", "2回戦", "Segunda ronda", "Zweite Runde"],
69
+ "Division Semifinals": ["Demi-finales de division", "ディビジョン準決勝", "Semifinales de división", "Division-Halbfinale"],
70
+ "Division Finals": ["Finales de division", "ディビジョン決勝", "Finales de división", "Division-Finale"],
71
+ "Conference Quarterfinals": ["Quarts de finale de conférence", "カンファレンス準々決勝", "Cuartos de final de conferencia", "Conference-Viertelfinale"],
72
+ "Conference Semifinals": ["Demi-finales de conférence", "カンファレンス準決勝", "Semifinales de conferencia", "Conference-Halbfinale"],
73
+ "Conference Finals": ["Finales de conférence", "カンファレンス決勝", "Finales de conferencia", "Conference-Finale"],
74
+ "Stanley Cup Semifinals": ["Demi-finales de la Coupe Stanley", "スタンレー・カップ準決勝", "Semifinales de la Stanley Cup", "Stanley-Cup-Halbfinale"],
75
+ "Stanley Cup Final": ["Finale de la Coupe Stanley", "スタンレー・カップ・ファイナル", "Final de la Stanley Cup", "Stanley-Cup-Finale"],
76
+ "NBA Finals": ["Finale de la NBA", "NBAファイナル", "Finales de la NBA", "NBA-Finale"],
77
+ "Wild Card Game": ["Match de wild card", "ワイルドカードゲーム", "Juego de comodín", "Wild-Card-Spiel"],
78
+ "Wild Card Series": ["Série de wild card", "ワイルドカードシリーズ", "Serie de comodín", "Wild-Card-Serie"],
79
+ "Division Series": ["Série de division", "ディビジョンシリーズ", "Serie divisional", "Division Series"],
80
+ "League Championship Series": ["Série de championnat", "リーグチャンピオンシップシリーズ", "Serie de Campeonato de Liga", "League Championship Series"],
81
+ "World Series": ["Série mondiale", "ワールドシリーズ", "Serie Mundial", "World Series"],
82
+ "Wild Card Round": ["Tour de wild card", "ワイルドカードラウンド", "Ronda de comodín", "Wild-Card-Runde"],
83
+ "Round One": ["Premier tour", "1回戦", "Primera ronda", "Erste Runde"],
84
+ "MLS Cup": ["Coupe MLS", "MLSカップ", "Copa MLS", "MLS Cup"],
85
+ "Wild Card": ["Wild card", "ワイルドカード・プレーオフ", "Comodín", "Wild-Card-Runde"],
86
+ "Divisional": ["Ronde divisionnaire", "ディビジョナル・プレーオフ", "Ronda divisional", "Divisional Round"],
87
+ "Divisional Playoffs": ["Ronde divisionnaire", "ディビジョナル・プレーオフ", "Ronda divisional", "Divisional Round"],
88
+ "Conference Championship": ["Finale de conférence", "カンファレンス・チャンピオンシップ", "Campeonato de conferencia", "Conference Championship"],
89
+ "Conference Championships": ["Finales de conférence", "カンファレンス・チャンピオンシップ", "Campeonatos de conferencia", "Conference Championships"],
90
+ "Super Bowl": ["Super Bowl", "スーパーボウル", "Super Bowl", "Super Bowl"],
91
+ "Division Semi-Final": ["Demi-finale de division", "ディビジョン準決勝", "Semifinal de división", "Division-Halbfinale"],
92
+ "Division Final": ["Finale de division", "ディビジョン決勝", "Final de división", "Division-Finale"],
93
+ "Grey Cup": ["Coupe Grey", "グレイ・カップ", "Grey Cup", "Grey Cup"],
94
+ };
95
+ const rounds = (index) => Object.fromEntries(Object.entries(ROUNDS).map(([en, names]) => [en, names[index]]));
96
+ /** The built-in languages, by code. */
97
+ export const LOCALES = Object.freeze({
98
+ en: { code: "en", tag: "en", words: EN, rounds: {} },
99
+ fr: { code: "fr", tag: "fr", words: FR, rounds: rounds(0) },
100
+ ja: { code: "ja", tag: "ja", words: JA, rounds: rounds(1) },
101
+ es: { code: "es", tag: "es", words: ES, rounds: rounds(2) },
102
+ de: { code: "de", tag: "de", words: DE, rounds: rounds(3) },
103
+ });
104
+ /** The codes of the built-in languages, English first. */
105
+ export const LOCALE_CODES = Object.freeze(["en", "fr", "ja", "es", "de"]);
106
+ /** The English words, as they were before there were languages. */
107
+ export const WORDS = LOCALES.en.words;
108
+ /**
109
+ * A language of your own: what you give, on top of the base language's (English unless `base` says another) for what you leave out.
110
+ * `tag` is what `Intl` formats numbers and dates with (default `code`).
111
+ */
112
+ export function defineLocale(code, parts, base = "en") {
113
+ const from = LOCALES[base];
114
+ return { code, tag: parts.tag ?? code, words: { ...from.words, ...parts.words }, rounds: { ...from.rounds, ...parts.rounds } };
115
+ }
116
+ /** The language an option names: a code of a built-in one, or a `Locale`. */
117
+ export function localeOf(option) {
118
+ if (option === undefined)
119
+ return LOCALES.en;
120
+ if (typeof option !== "string")
121
+ return option;
122
+ const hit = LOCALES[option] ?? LOCALES[option.split("-")[0]];
123
+ if (!hit)
124
+ throw new Error(`taisen-brackets: no language ${JSON.stringify(option)}; the built-in ones are ${LOCALE_CODES.join(", ")} (defineLocale makes another)`);
125
+ return hit;
126
+ }
127
+ export function voiceOf(options) {
128
+ const locale = localeOf(options.locale);
129
+ const words = options.words ? { ...locale.words, ...options.words } : locale.words;
130
+ const numbers = new Intl.NumberFormat(locale.tag);
131
+ const dates = new Intl.DateTimeFormat(locale.tag, { dateStyle: "medium", timeZone: "UTC" });
132
+ return {
133
+ locale,
134
+ words,
135
+ number: (n) => numbers.format(n),
136
+ date: (iso) => {
137
+ const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
138
+ return m ? dates.format(new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])))) : null;
139
+ },
140
+ upper: (text) => (locale.code === "ja" ? text : text.toLocaleUpperCase(locale.tag)),
141
+ round: (name) => {
142
+ const hit = locale.rounds[name];
143
+ if (hit !== undefined)
144
+ return hit;
145
+ const of = /^Round of (\d+)$/.exec(name);
146
+ return of ? fill(words.roundOf, { n: of[1] }) : name;
147
+ },
148
+ team: (team) => options.teamName?.(team, locale) ?? team.names?.[locale.code] ?? team.name,
149
+ };
150
+ }
151
+ /** Fills `{n}`-style holes in a phrase. */
152
+ export function fill(phrase, values) {
153
+ return phrase.replace(/\{(\w+)\}/g, (hole, key) => (key in values ? String(values[key]) : hole));
154
+ }
155
+ const SUBDIVISIONS = {
156
+ "GB-ENG": { en: "England", fr: "Angleterre", ja: "イングランド", es: "Inglaterra", de: "England" },
157
+ "GB-SCT": { en: "Scotland", fr: "Écosse", ja: "スコットランド", es: "Escocia", de: "Schottland" },
158
+ "GB-WLS": { en: "Wales", fr: "Pays de Galles", ja: "ウェールズ", es: "Gales", de: "Wales" },
159
+ "GB-NIR": { en: "Northern Ireland", fr: "Irlande du Nord", ja: "北アイルランド", es: "Irlanda del Norte", de: "Nordirland" },
160
+ };
161
+ /**
162
+ * The name of a country in a language, from its ISO 3166 code, through `Intl.DisplayNames`: no list to write ("DE" is
163
+ * Germany, Allemagne, ドイツ, Alemania, Deutschland). The home nations (`GB-ENG`, `GB-SCT`, `GB-WLS`, `GB-NIR`) are not regions
164
+ * to `Intl`, so they have names here. A code `Intl` does not know gives `undefined`.
165
+ */
166
+ export function countryName(code, locale = "en") {
167
+ const l = localeOf(locale);
168
+ const sub = SUBDIVISIONS[code.toUpperCase()];
169
+ if (sub)
170
+ return sub[l.code] ?? sub.en;
171
+ try {
172
+ const names = new Intl.DisplayNames([l.tag], { type: "region" });
173
+ const name = names.of(code.toUpperCase());
174
+ return name && name !== code.toUpperCase() ? name : undefined;
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ }