feat(assistant): passport, country and bucket list cards

This commit is contained in:
Maurice
2026-10-22 15:50:54 +02:00
parent fd15a7343e
commit da7af8337d
57 changed files with 4564 additions and 72 deletions
+21
View File
@@ -0,0 +1,21 @@
// FE-UTIL-COUNTRYNAME-001 to FE-UTIL-COUNTRYNAME-002
import { describe, expect, it } from 'vitest';
import { countryNamer } from './countryName';
const countryName = (code: string, language: string) => countryNamer(language)(code);
describe('countryName', () => {
it('FE-UTIL-COUNTRYNAME-001: a code reads as the country in the reader language, whatever its case', () => {
expect(countryName('JP', 'en')).toBe('Japan');
expect(countryName('fr', 'de')).toBe('Frankreich');
const name = countryNamer('es');
expect(name('DE')).toBe('Alemania');
});
it('FE-UTIL-COUNTRYNAME-002: something that is no country code stays as it came', () => {
expect(countryName('JPN', 'en')).toBe('JPN');
expect(countryName('', 'en')).toBe('');
expect(countryName('JP', 'not-a-language')).toBe('Japan');
});
});
+25
View File
@@ -0,0 +1,25 @@
import { getIntlLanguage } from '@trek/shared';
/**
* Country names in the reader's language, from ISO alpha-2 codes. A namer is
* built once per language (the caller keeps it, a component in a memo) and
* answers the code itself where the runtime knows no name or the code is
* none, so a card never shows an empty label.
*/
export function countryNamer(language: string): (code: string) => string {
let names: Intl.DisplayNames | null = null;
try {
names = new Intl.DisplayNames([getIntlLanguage(language)], { type: 'region' });
} catch {
names = null;
}
return (code) => {
const upper = code.toUpperCase();
if (!names || !/^[A-Z]{2}$/.test(upper)) return code;
try {
return names.of(upper) ?? code;
} catch {
return code;
}
};
}