fix: restore automatic language flags

This commit is contained in:
2026-09-16 10:26:11 +03:30
parent a3a49adad2
commit 9119eb6c29
279 changed files with 10816 additions and 6 deletions

View File

@@ -3,7 +3,9 @@
import { useEffect, useRef, useState } from 'react';
import { portfolioPath } from '@/content/contracts';
import Link from 'next/link';
import { Check, Languages, Moon, Sun } from 'lucide-react';
import Image from 'next/image';
import { languageFlag } from '@/content/language-flags';
import { Check, Globe, Languages, Moon, Sun } from 'lucide-react';
import { useTheme } from '@/context/ThemeContext';
import type { PortfolioProps } from '@/server/content';
import styles from './HeroSection.module.css';
@@ -45,12 +47,17 @@ export function HeroControls({ portfolio }: PortfolioProps) {
<Languages size={23} strokeWidth={1.9} aria-hidden="true" />
</button>
<div id="language-menu" className={styles.languageMenu} role="menu" aria-label={copy.languageMenuLabel} data-open={isOpen}>
{portfolio.languages.map(language => <Link key={language.code}
{portfolio.languages.map(language => {
const flag = languageFlag(language.code, language.flagCountry);
return <Link key={language.code}
href={portfolioPath(language.code, portfolio.defaultLanguage)} hrefLang={language.code} role="menuitem"
aria-current={locale === language.code ? 'page' : undefined} className={styles.languageOption}>
<span className={styles.languageOptionLabel} lang={language.code} dir={language.direction}>{language.name}</span>
<span className={styles.languageOptionLabel}>
{flag ? <Image src={flag} alt="" aria-hidden="true" width={20} height={20} unoptimized className={styles.languageFlag} /> : <Globe size={20} aria-hidden="true" className={styles.languageFlag} />}
<span lang={language.code} dir={language.direction}>{language.name}</span>
</span>
{locale === language.code && <Check size={16} className={styles.selectedIcon} aria-hidden="true" />}
</Link>)}
</Link>; })}
</div>
</div>
</div>

View File

@@ -53,6 +53,7 @@ export const languageSchema = z.object({
try { return Intl.getCanonicalLocales(value)[0].toLowerCase() === value && !['admin','api','media','projects'].includes(value); } catch { return false; }
}, 'Use a lowercase language tag, such as en, fa, or pt-br'),
name: title, direction: z.enum(['ltr', 'rtl']), enabled: z.boolean(),
flagCountry: z.string().regex(/^(?:[a-z]{2})?$/, 'Use a lowercase country code, such as gb or us, or leave blank for automatic').optional(),
messages: z.record(z.string().max(150), z.string().max(2000)),
}).strict();
export const entrySchema = z.object({

View File

@@ -0,0 +1 @@
["ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as", "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bl", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cp", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dg", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "eu", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "ic", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pc", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "ss", "st", "sv", "sx", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "un", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "xk", "xx", "ye", "yt", "za", "zm", "zw"]

View File

@@ -0,0 +1,14 @@
import countries from './flag-countries.json';
const available = new Set(countries);
/** Explicit preference wins; preserve the original flags for the two built-in languages. */
export function languageFlag(code: string, country?: string): string | null {
if (!country && code === 'en') return '/images/flags/english.svg';
if (!country && code === 'fa') return '/images/flags/persian.svg';
try {
const locale = new Intl.Locale(code);
const region = (country || locale.region || locale.maximize().region)?.toLowerCase();
return region && available.has(region) ? `/images/flags/countries/${region}.svg` : null;
} catch { return null; }
}

View File

@@ -37,7 +37,7 @@ export function publicContent(snapshot: ContentSnapshot, locale: string) {
})) as EntryData<K>[];
return {
locale, direction: language.direction, defaultLanguage: settings.defaultLanguage,
languages: snapshot.languages.filter(item => item.enabled).map(({ code, name, direction }) => ({ code, name, direction })),
languages: snapshot.languages.filter(item => item.enabled).map(({ code, name, direction, flagCountry }) => ({ code, name, direction, flagCountry })),
profile: { avatar: settings.avatar, email: settings.email, contacts: settings.contacts, contactUrl: settings.contactUrl, blogUrl: settings.blogUrl, coursesUrl: settings.coursesUrl },
metadata: settings.metadata[locale] ?? settings.metadata[settings.defaultLanguage],
messages: {