Shipping a sports app in Nahuatl, Maya, Tzeltal and Tzotzil: i18n lessons from 60 locales
We build MeetAndPlay, a padel, tennis and pickleball matchmaking and court-booking app that launches in Cancún on 1 December 2026. Here is what we learned adding indigenous Mexican languages to a Next.js app, including t
We build MeetAndPlay, a padel, tennis and pickleball matchmaking and court-booking app that launches in Cancún on 1 December 2026. Here is what we learned adding indigenous Mexican languages to a Next.js app, including the parts that are still unfinished.
When we started, the plan was ordinary: Spanish first, English for tourists, a handful of European languages for the people who fly into Cancún. Then a directive changed the scope: support every language that is actually popular in Mexico. Taken literally, that does not mean French and Dutch. It means Nahuatl, Yucatec Maya, Tzeltal, Tzotzil and a long tail after them.
The locale list started smaller and has grown to 60 tags today, so treat any number in this post as a moving target. This post is about the engineering and the judgment calls, written for anyone who has to ship a language that your tooling does not know exists. It is also about what we have not done, because that is the part most i18n posts skip.
If you want to see the result, the MeetAndPlay site has a language switcher where the indigenous languages sit in their own group.
Why these languages at all
The criterion we ended up with is demographic, and it comes from Mexico's statistics institute. INEGI's 2020 census counted 7,364,645 people aged three and over who speak an indigenous language, across 68 languages. About 866,000 of them (11.8%) do not speak Spanish. Nahuatl has 1,651,958 speakers, Maya (Yucatec) 774,755, Tseltal 589,144 and Tsotsil 550,274.
For a Cancún product, two of those matter for a reason beyond the national numbers. Yucatec Maya is native to Quintana Roo. And in our own research notes, Tseltal and Tsotsil came out as the second and third most spoken indigenous languages in Cancún after Maya (4,098 and 3,208 speakers in the Benito Juárez municipality), with the highest monolingual rates of the four. Those are Chiapas migrants who work in hospitality, including at sports clubs. For them a Spanish-only screen may be no screen at all.
We are not claiming these readers are our customers today; a product built for a Mexican city should simply be usable by the people who live in it.
Lesson 1: locale tags are a permanent public decision
A locale tag ends up in four places you cannot easily change: the URL segment, <html lang>, hreflang, and Open Graph's og:locale. Once other sites link to /xyz/, renaming it costs real money. So we audited every tag against two registers, the ISO 639-3 code set and the IANA language subtag registry, before shipping any copy. Three findings, in increasing order of annoyance:
Some languages map to one code cleanly. yua (Yucatec Maya), tzh (Tzeltal) and tzo (Tzotzil) are individual languages with one code each. Easy.
Nahuatl has no individual code. nah is a collection code: ISO 639-3 splits Nahuatl into about 29 individual languages and none of them is "Nahuatl". RFC 5646 says a collection subtag may be used when more specific information is unavailable, but that it SHOULD NOT be used for content better tagged with an individual language. We kept nah anyway, because picking one variety (say, the Huasteca one) would tell a reader from Guerrero that we chose someone else's Nahuatl for them. The cost is real and we document it: a browser sending Accept-Language: nhe will not match nah. Content negotiation is where this bites.
One of our tags was simply the wrong language. mix looks like "Mixtec" but ISO 639-3 defines it as Mixtepec Mixtec, the speech of one municipality in Oaxaca, with on the order of ten thousand speakers. Mixtec as a whole has over half a million speakers in the census and 52 individual codes with no macrolanguage above them. If we ever served <html lang="mix"> for a translated Mixtec site, we would tell screen readers and search engines something false about roughly 98% of the intended readers. It is still open as a decision; the lesson is that a tag that "looks right" is not a tag that has been checked.
A smaller one: ISO prints "Tzeltal" and "Tzotzil"; the modern INALI orthography is "Tseltal" and "Tsotsil". We show the latter in the switcher and keep the ISO codes in the markup.
Lesson 2: your date and number libraries do not know these languages
Not one indigenous tag we ship has CLDR date or number data, and only Nahuatl has plural rules. Intl.DateTimeFormat("tzh") falls back silently, usually to the runtime default, which is not what you want. We route every date and number through a formattingLocale() helper that maps each locale to the nearest language that has data. For these audiences that means formatting like Mexican Spanish. It is a pragmatic answer, not a right one; native speakers should decide how a date reads.
Plural rules are the quieter problem: for a language with no CLDR rules the engine falls back to a default that may be wrong, so we avoid plural-sensitive phrasing where we can, and our check script treats broken ICU syntax as a hard error.
Lesson 3: honest status beats a complete-looking list
This is the part we most want other teams to copy. The tempting move is to generate a catalogue for every language and ship it. A machine-drafted sentence in a language the model barely knows can be confidently wrong, and the reader cannot tell. So each locale has an explicit status:
- reviewed: a human who reads the language checked the catalogue. Seven locales: Spanish, English, Portuguese, French, German, Italian, Russian.
- machine: generated and not reviewed by a native speaker. It carries a "beta" badge in the switcher. Nineteen locales are here, including Chinese, Korean, Japanese, Dutch, Arabic, Yucatec Maya, Nahuatl, Tzeltal and Tzotzil.
- pending: the file exists, but its contents are a structural copy of the fallback language. For a Mexican indigenous language the fallback is Spanish, because those readers already navigate in Spanish, and a Spanish screen is more honest than invented text.
Thirty-four indigenous catalogues are in the pending state today (7 reviewed + 19 machine + 34 pending = 60). They are in the list because the directive was to cover the languages, and they are pending because we would rather say so than pretend.
The part that matters for engineering is that "pending" is derived, not hand-flagged. A pending locale renders <html lang="es-MX">, is excluded from hreflang and the sitemap, gets noindex, and reports og:locale as es_MX, so the markup never claims a language the page does not contain. The trap: the moment someone translates a pending file, the whole lang/hreflang/sitemap cluster switches on automatically, and nobody re-decides the tag. That is why the mix problem matters now, while it is cheap to fix.
Lesson 4: make the build enforce the boring invariants
You cannot review 60 catalogues by eye. Our check script treats key parity, placeholder parity ({count}, {club}) and valid ICU syntax as hard errors in every locale. For pending locales it downgrades only two rule families to warnings: "identical to another locale" and CJK script purity. That lets us hold Spanish text in a Tzeltal file without failing the build, while still failing a catalogue that drops a key or a placeholder.
One pitfall worth stealing: surfaces like our OG cards and the WhatsApp share layer once kept their own hand-maintained list of pending locales, and went stale on every promotion. We now derive every such list from the single status file and pin it with a test.
What we have not done
Being straight about this, because writing "60 locales" in a headline invites a different reading than the truth.
- No native-speaker review yet for any indigenous locale. The status is machine, and it says beta on the screen. Finding reviewers is the actual work.
- Dozens of locales are Spanish text in disguise. They are a commitment and a structure, not a finished product.
- Not every language is covered. We excluded languages when ISO 639-3 splits them into many coordinate varieties with no obvious standard, and later accepted that risk for some by choosing the largest variety, with a requirement that a native reviewer confirms it before promotion. That decision can be wrong, and we say so in the repo.
- We have no usage numbers for these locales, and will not make any up.
If you are a speaker of Nahuatl, Yucatec Maya, Tseltal or Tsotsil and want to correct our strings, we would like to hear from you at [email protected].
Takeaways
Audit tags against ISO 639-3 and IANA before building on them; keep each catalogue's status in one file and derive everything else from it; prefer a labeled fallback to confident gibberish; and test keys, placeholders and ICU in every locale. The app is MeetAndPlay: find a game, find players, book a court.
Sources: INEGI, Censo de Población y Vivienda 2020 (press release 430/22); ISO 639-3 code set (SIL); IANA Language Subtag Registry; RFC 5646.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.