fix(ui): полный справочник стран и городов как в VPS Tracker
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 4s
quality / docker-check (push) Skipped
quality / web (push) Successful in 45s
quality / api (push) Successful in 36s
CD / quality (push) Successful in 1m30s
CD / publish (push) Successful in 1m51s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-04 15:15:34 +07:00
co-authored by Cursor
parent 146afc1369
commit 654b08c8e5
18 changed files with 1400 additions and 66 deletions
+467
View File
@@ -0,0 +1,467 @@
// src/geo/countries.ts
var COUNTRIES = [
{ code: "AU", name: "\u0410\u0432\u0441\u0442\u0440\u0430\u043B\u0438\u044F", nameEn: "Australia" },
{ code: "AT", name: "\u0410\u0432\u0441\u0442\u0440\u0438\u044F", nameEn: "Austria" },
{ code: "AZ", name: "\u0410\u0437\u0435\u0440\u0431\u0430\u0439\u0434\u0436\u0430\u043D", nameEn: "Azerbaijan" },
{ code: "AL", name: "\u0410\u043B\u0431\u0430\u043D\u0438\u044F", nameEn: "Albania" },
{ code: "DZ", name: "\u0410\u043B\u0436\u0438\u0440", nameEn: "Algeria" },
{ code: "AO", name: "\u0410\u043D\u0433\u043E\u043B\u0430", nameEn: "Angola" },
{ code: "AR", name: "\u0410\u0440\u0433\u0435\u043D\u0442\u0438\u043D\u0430", nameEn: "Argentina" },
{ code: "AM", name: "\u0410\u0440\u043C\u0435\u043D\u0438\u044F", nameEn: "Armenia" },
{ code: "AF", name: "\u0410\u0444\u0433\u0430\u043D\u0438\u0441\u0442\u0430\u043D", nameEn: "Afghanistan" },
{ code: "BE", name: "\u0411\u0435\u043B\u044C\u0433\u0438\u044F", nameEn: "Belgium" },
{ code: "BG", name: "\u0411\u043E\u043B\u0433\u0430\u0440\u0438\u044F", nameEn: "Bulgaria" },
{ code: "BO", name: "\u0411\u043E\u043B\u0438\u0432\u0438\u044F", nameEn: "Bolivia" },
{ code: "BA", name: "\u0411\u043E\u0441\u043D\u0438\u044F \u0438 \u0413\u0435\u0440\u0446\u0435\u0433\u043E\u0432\u0438\u043D\u0430", nameEn: "Bosnia and Herzegovina" },
{ code: "BR", name: "\u0411\u0440\u0430\u0437\u0438\u043B\u0438\u044F", nameEn: "Brazil" },
{ code: "GB", name: "\u0412\u0435\u043B\u0438\u043A\u043E\u0431\u0440\u0438\u0442\u0430\u043D\u0438\u044F", nameEn: "United Kingdom" },
{ code: "HU", name: "\u0412\u0435\u043D\u0433\u0440\u0438\u044F", nameEn: "Hungary" },
{ code: "VE", name: "\u0412\u0435\u043D\u0435\u0441\u0443\u044D\u043B\u0430", nameEn: "Venezuela" },
{ code: "VN", name: "\u0412\u044C\u0435\u0442\u043D\u0430\u043C", nameEn: "Vietnam" },
{ code: "GA", name: "\u0413\u0430\u0431\u043E\u043D", nameEn: "Gabon" },
{ code: "HT", name: "\u0413\u0430\u0438\u0442\u0438", nameEn: "Haiti" },
{ code: "GY", name: "\u0413\u0430\u0439\u0430\u043D\u0430", nameEn: "Guyana" },
{ code: "GM", name: "\u0413\u0430\u043C\u0431\u0438\u044F", nameEn: "Gambia" },
{ code: "GH", name: "\u0413\u0430\u043D\u0430", nameEn: "Ghana" },
{ code: "GT", name: "\u0413\u0432\u0430\u0442\u0435\u043C\u0430\u043B\u0430", nameEn: "Guatemala" },
{ code: "GN", name: "\u0413\u0432\u0438\u043D\u0435\u044F", nameEn: "Guinea" },
{ code: "DE", name: "\u0413\u0435\u0440\u043C\u0430\u043D\u0438\u044F", nameEn: "Germany" },
{ code: "HN", name: "\u0413\u043E\u043D\u0434\u0443\u0440\u0430\u0441", nameEn: "Honduras" },
{ code: "GR", name: "\u0413\u0440\u0435\u0446\u0438\u044F", nameEn: "Greece" },
{ code: "GE", name: "\u0413\u0440\u0443\u0437\u0438\u044F", nameEn: "Georgia" },
{ code: "DK", name: "\u0414\u0430\u043D\u0438\u044F", nameEn: "Denmark" },
{ code: "CD", name: "\u0414\u0420 \u041A\u043E\u043D\u0433\u043E", nameEn: "DR Congo" },
{ code: "EG", name: "\u0415\u0433\u0438\u043F\u0435\u0442", nameEn: "Egypt" },
{ code: "ZM", name: "\u0417\u0430\u043C\u0431\u0438\u044F", nameEn: "Zambia" },
{ code: "ZW", name: "\u0417\u0438\u043C\u0431\u0430\u0431\u0432\u0435", nameEn: "Zimbabwe" },
{ code: "IL", name: "\u0418\u0437\u0440\u0430\u0438\u043B\u044C", nameEn: "Israel" },
{ code: "IN", name: "\u0418\u043D\u0434\u0438\u044F", nameEn: "India" },
{ code: "ID", name: "\u0418\u043D\u0434\u043E\u043D\u0435\u0437\u0438\u044F", nameEn: "Indonesia" },
{ code: "JO", name: "\u0418\u043E\u0440\u0434\u0430\u043D\u0438\u044F", nameEn: "Jordan" },
{ code: "IQ", name: "\u0418\u0440\u0430\u043A", nameEn: "Iraq" },
{ code: "IR", name: "\u0418\u0440\u0430\u043D", nameEn: "Iran" },
{ code: "IE", name: "\u0418\u0440\u043B\u0430\u043D\u0434\u0438\u044F", nameEn: "Ireland" },
{ code: "IS", name: "\u0418\u0441\u043B\u0430\u043D\u0434\u0438\u044F", nameEn: "Iceland" },
{ code: "ES", name: "\u0418\u0441\u043F\u0430\u043D\u0438\u044F", nameEn: "Spain" },
{ code: "IT", name: "\u0418\u0442\u0430\u043B\u0438\u044F", nameEn: "Italy" },
{ code: "YE", name: "\u0419\u0435\u043C\u0435\u043D", nameEn: "Yemen" },
{ code: "KZ", name: "\u041A\u0430\u0437\u0430\u0445\u0441\u0442\u0430\u043D", nameEn: "Kazakhstan" },
{ code: "KH", name: "\u041A\u0430\u043C\u0431\u043E\u0434\u0436\u0430", nameEn: "Cambodia" },
{ code: "CM", name: "\u041A\u0430\u043C\u0435\u0440\u0443\u043D", nameEn: "Cameroon" },
{ code: "CA", name: "\u041A\u0430\u043D\u0430\u0434\u0430", nameEn: "Canada" },
{ code: "QA", name: "\u041A\u0430\u0442\u0430\u0440", nameEn: "Qatar" },
{ code: "KE", name: "\u041A\u0435\u043D\u0438\u044F", nameEn: "Kenya" },
{ code: "CY", name: "\u041A\u0438\u043F\u0440", nameEn: "Cyprus" },
{ code: "KG", name: "\u041A\u0438\u0440\u0433\u0438\u0437\u0438\u044F", nameEn: "Kyrgyzstan" },
{ code: "CN", name: "\u041A\u0438\u0442\u0430\u0439", nameEn: "China" },
{ code: "CO", name: "\u041A\u043E\u043B\u0443\u043C\u0431\u0438\u044F", nameEn: "Colombia" },
{ code: "CG", name: "\u041A\u043E\u043D\u0433\u043E", nameEn: "Congo" },
{ code: "KR", name: "\u042E\u0436\u043D\u0430\u044F \u041A\u043E\u0440\u0435\u044F", nameEn: "South Korea" },
{ code: "KP", name: "\u0421\u0435\u0432\u0435\u0440\u043D\u0430\u044F \u041A\u043E\u0440\u0435\u044F", nameEn: "North Korea" },
{ code: "XK", name: "\u041A\u043E\u0441\u043E\u0432\u043E", nameEn: "Kosovo" },
{ code: "CR", name: "\u041A\u043E\u0441\u0442\u0430-\u0420\u0438\u043A\u0430", nameEn: "Costa Rica" },
{ code: "CI", name: "\u041A\u043E\u0442-\u0434'\u0418\u0432\u0443\u0430\u0440", nameEn: "C\xF4te d'Ivoire" },
{ code: "CU", name: "\u041A\u0443\u0431\u0430", nameEn: "Cuba" },
{ code: "KW", name: "\u041A\u0443\u0432\u0435\u0439\u0442", nameEn: "Kuwait" },
{ code: "LA", name: "\u041B\u0430\u043E\u0441", nameEn: "Laos" },
{ code: "LV", name: "\u041B\u0430\u0442\u0432\u0438\u044F", nameEn: "Latvia" },
{ code: "LR", name: "\u041B\u0438\u0431\u0435\u0440\u0438\u044F", nameEn: "Liberia" },
{ code: "LB", name: "\u041B\u0438\u0432\u0430\u043D", nameEn: "Lebanon" },
{ code: "LY", name: "\u041B\u0438\u0432\u0438\u044F", nameEn: "Libya" },
{ code: "LT", name: "\u041B\u0438\u0442\u0432\u0430", nameEn: "Lithuania" },
{ code: "LI", name: "\u041B\u0438\u0445\u0442\u0435\u043D\u0448\u0442\u0435\u0439\u043D", nameEn: "Liechtenstein" },
{ code: "LU", name: "\u041B\u044E\u043A\u0441\u0435\u043C\u0431\u0443\u0440\u0433", nameEn: "Luxembourg" },
{ code: "MU", name: "\u041C\u0430\u0432\u0440\u0438\u043A\u0438\u0439", nameEn: "Mauritius" },
{ code: "MR", name: "\u041C\u0430\u0432\u0440\u0438\u0442\u0430\u043D\u0438\u044F", nameEn: "Mauritania" },
{ code: "MG", name: "\u041C\u0430\u0434\u0430\u0433\u0430\u0441\u043A\u0430\u0440", nameEn: "Madagascar" },
{ code: "MO", name: "\u041C\u0430\u043A\u0430\u043E", nameEn: "Macao" },
{ code: "MW", name: "\u041C\u0430\u043B\u0430\u0432\u0438", nameEn: "Malawi" },
{ code: "MY", name: "\u041C\u0430\u043B\u0430\u0439\u0437\u0438\u044F", nameEn: "Malaysia" },
{ code: "ML", name: "\u041C\u0430\u043B\u0438", nameEn: "Mali" },
{ code: "MT", name: "\u041C\u0430\u043B\u044C\u0442\u0430", nameEn: "Malta" },
{ code: "MA", name: "\u041C\u0430\u0440\u043E\u043A\u043A\u043E", nameEn: "Morocco" },
{ code: "MX", name: "\u041C\u0435\u043A\u0441\u0438\u043A\u0430", nameEn: "Mexico" },
{ code: "MZ", name: "\u041C\u043E\u0437\u0430\u043C\u0431\u0438\u043A", nameEn: "Mozambique" },
{ code: "MD", name: "\u041C\u043E\u043B\u0434\u0430\u0432\u0438\u044F", nameEn: "Moldova" },
{ code: "MC", name: "\u041C\u043E\u043D\u0430\u043A\u043E", nameEn: "Monaco" },
{ code: "MN", name: "\u041C\u043E\u043D\u0433\u043E\u043B\u0438\u044F", nameEn: "Mongolia" },
{ code: "MM", name: "\u041C\u044C\u044F\u043D\u043C\u0430", nameEn: "Myanmar" },
{ code: "NA", name: "\u041D\u0430\u043C\u0438\u0431\u0438\u044F", nameEn: "Namibia" },
{ code: "NP", name: "\u041D\u0435\u043F\u0430\u043B", nameEn: "Nepal" },
{ code: "NE", name: "\u041D\u0438\u0433\u0435\u0440", nameEn: "Niger" },
{ code: "NG", name: "\u041D\u0438\u0433\u0435\u0440\u0438\u044F", nameEn: "Nigeria" },
{ code: "NL", name: "\u041D\u0438\u0434\u0435\u0440\u043B\u0430\u043D\u0434\u044B", nameEn: "Netherlands" },
{ code: "NI", name: "\u041D\u0438\u043A\u0430\u0440\u0430\u0433\u0443\u0430", nameEn: "Nicaragua" },
{ code: "NZ", name: "\u041D\u043E\u0432\u0430\u044F \u0417\u0435\u043B\u0430\u043D\u0434\u0438\u044F", nameEn: "New Zealand" },
{ code: "NO", name: "\u041D\u043E\u0440\u0432\u0435\u0433\u0438\u044F", nameEn: "Norway" },
{ code: "AE", name: "\u041E\u0410\u042D", nameEn: "United Arab Emirates" },
{ code: "OM", name: "\u041E\u043C\u0430\u043D", nameEn: "Oman" },
{ code: "PK", name: "\u041F\u0430\u043A\u0438\u0441\u0442\u0430\u043D", nameEn: "Pakistan" },
{ code: "PA", name: "\u041F\u0430\u043D\u0430\u043C\u0430", nameEn: "Panama" },
{ code: "PG", name: "\u041F\u0430\u043F\u0443\u0430 \u2014 \u041D\u043E\u0432\u0430\u044F \u0413\u0432\u0438\u043D\u0435\u044F", nameEn: "Papua New Guinea" },
{ code: "PY", name: "\u041F\u0430\u0440\u0430\u0433\u0432\u0430\u0439", nameEn: "Paraguay" },
{ code: "PE", name: "\u041F\u0435\u0440\u0443", nameEn: "Peru" },
{ code: "PL", name: "\u041F\u043E\u043B\u044C\u0448\u0430", nameEn: "Poland" },
{ code: "PT", name: "\u041F\u043E\u0440\u0442\u0443\u0433\u0430\u043B\u0438\u044F", nameEn: "Portugal" },
{ code: "RU", name: "\u0420\u043E\u0441\u0441\u0438\u044F", nameEn: "Russia" },
{ code: "RW", name: "\u0420\u0443\u0430\u043D\u0434\u0430", nameEn: "Rwanda" },
{ code: "RO", name: "\u0420\u0443\u043C\u044B\u043D\u0438\u044F", nameEn: "Romania" },
{ code: "SV", name: "\u0421\u0430\u043B\u044C\u0432\u0430\u0434\u043E\u0440", nameEn: "El Salvador" },
{ code: "SA", name: "\u0421\u0430\u0443\u0434\u043E\u0432\u0441\u043A\u0430\u044F \u0410\u0440\u0430\u0432\u0438\u044F", nameEn: "Saudi Arabia" },
{ code: "MK", name: "\u0421\u0435\u0432\u0435\u0440\u043D\u0430\u044F \u041C\u0430\u043A\u0435\u0434\u043E\u043D\u0438\u044F", nameEn: "North Macedonia" },
{ code: "SC", name: "\u0421\u0435\u0439\u0448\u0435\u043B\u044B", nameEn: "Seychelles" },
{ code: "SN", name: "\u0421\u0435\u043D\u0435\u0433\u0430\u043B", nameEn: "Senegal" },
{ code: "RS", name: "\u0421\u0435\u0440\u0431\u0438\u044F", nameEn: "Serbia" },
{ code: "SG", name: "\u0421\u0438\u043D\u0433\u0430\u043F\u0443\u0440", nameEn: "Singapore" },
{ code: "SK", name: "\u0421\u043B\u043E\u0432\u0430\u043A\u0438\u044F", nameEn: "Slovakia" },
{ code: "SI", name: "\u0421\u043B\u043E\u0432\u0435\u043D\u0438\u044F", nameEn: "Slovenia" },
{ code: "SO", name: "\u0421\u043E\u043C\u0430\u043B\u0438", nameEn: "Somalia" },
{ code: "SD", name: "\u0421\u0443\u0434\u0430\u043D", nameEn: "Sudan" },
{ code: "US", name: "\u0421\u0428\u0410", nameEn: "United States" },
{ code: "TJ", name: "\u0422\u0430\u0434\u0436\u0438\u043A\u0438\u0441\u0442\u0430\u043D", nameEn: "Tajikistan" },
{ code: "TH", name: "\u0422\u0430\u0438\u043B\u0430\u043D\u0434", nameEn: "Thailand" },
{ code: "TZ", name: "\u0422\u0430\u043D\u0437\u0430\u043D\u0438\u044F", nameEn: "Tanzania" },
{ code: "TG", name: "\u0422\u043E\u0433\u043E", nameEn: "Togo" },
{ code: "TT", name: "\u0422\u0440\u0438\u043D\u0438\u0434\u0430\u0434 \u0438 \u0422\u043E\u0431\u0430\u0433\u043E", nameEn: "Trinidad and Tobago" },
{ code: "TV", name: "\u0422\u0443\u0432\u0430\u043B\u0443", nameEn: "Tuvalu" },
{ code: "TN", name: "\u0422\u0443\u043D\u0438\u0441", nameEn: "Tunisia" },
{ code: "TM", name: "\u0422\u0443\u0440\u043A\u043C\u0435\u043D\u0438\u044F", nameEn: "Turkmenistan" },
{ code: "TR", name: "\u0422\u0443\u0440\u0446\u0438\u044F", nameEn: "Turkey" },
{ code: "UG", name: "\u0423\u0433\u0430\u043D\u0434\u0430", nameEn: "Uganda" },
{ code: "UZ", name: "\u0423\u0437\u0431\u0435\u043A\u0438\u0441\u0442\u0430\u043D", nameEn: "Uzbekistan" },
{ code: "UA", name: "\u0423\u043A\u0440\u0430\u0438\u043D\u0430", nameEn: "Ukraine" },
{ code: "UY", name: "\u0423\u0440\u0443\u0433\u0432\u0430\u0439", nameEn: "Uruguay" },
{ code: "FJ", name: "\u0424\u0438\u0434\u0436\u0438", nameEn: "Fiji" },
{ code: "PH", name: "\u0424\u0438\u043B\u0438\u043F\u043F\u0438\u043D\u044B", nameEn: "Philippines" },
{ code: "FI", name: "\u0424\u0438\u043D\u043B\u044F\u043D\u0434\u0438\u044F", nameEn: "Finland" },
{ code: "FR", name: "\u0424\u0440\u0430\u043D\u0446\u0438\u044F", nameEn: "France" },
{ code: "HR", name: "\u0425\u043E\u0440\u0432\u0430\u0442\u0438\u044F", nameEn: "Croatia" },
{ code: "TD", name: "\u0427\u0430\u0434", nameEn: "Chad" },
{ code: "ME", name: "\u0427\u0435\u0440\u043D\u043E\u0433\u043E\u0440\u0438\u044F", nameEn: "Montenegro" },
{ code: "CZ", name: "\u0427\u0435\u0445\u0438\u044F", nameEn: "Czechia" },
{ code: "CL", name: "\u0427\u0438\u043B\u0438", nameEn: "Chile" },
{ code: "CH", name: "\u0428\u0432\u0435\u0439\u0446\u0430\u0440\u0438\u044F", nameEn: "Switzerland" },
{ code: "SE", name: "\u0428\u0432\u0435\u0446\u0438\u044F", nameEn: "Sweden" },
{ code: "LK", name: "\u0428\u0440\u0438-\u041B\u0430\u043D\u043A\u0430", nameEn: "Sri Lanka" },
{ code: "EC", name: "\u042D\u043A\u0432\u0430\u0434\u043E\u0440", nameEn: "Ecuador" },
{ code: "GQ", name: "\u042D\u043A\u0432\u0430\u0442\u043E\u0440\u0438\u0430\u043B\u044C\u043D\u0430\u044F \u0413\u0432\u0438\u043D\u0435\u044F", nameEn: "Equatorial Guinea" },
{ code: "ER", name: "\u042D\u0440\u0438\u0442\u0440\u0435\u044F", nameEn: "Eritrea" },
{ code: "EE", name: "\u042D\u0441\u0442\u043E\u043D\u0438\u044F", nameEn: "Estonia" },
{ code: "ET", name: "\u042D\u0444\u0438\u043E\u043F\u0438\u044F", nameEn: "Ethiopia" },
{ code: "ZA", name: "\u042E\u0410\u0420", nameEn: "South Africa" },
{ code: "JM", name: "\u042F\u043C\u0430\u0439\u043A\u0430", nameEn: "Jamaica" },
{ code: "JP", name: "\u042F\u043F\u043E\u043D\u0438\u044F", nameEn: "Japan" }
];
var COUNTRY_BY_CODE = Object.fromEntries(
COUNTRIES.map((c) => [c.code, c])
);
var COUNTRY_BY_NAME_RU = Object.fromEntries(
COUNTRIES.map((c) => [c.name.toLowerCase(), c])
);
// src/geo/cities.ts
var CITIES = [
// Россия
{ countryCode: "RU", name: "\u041C\u043E\u0441\u043A\u0432\u0430", nameEn: "Moscow" },
{ countryCode: "RU", name: "\u0421\u0430\u043D\u043A\u0442-\u041F\u0435\u0442\u0435\u0440\u0431\u0443\u0440\u0433", nameEn: "Saint Petersburg" },
{ countryCode: "RU", name: "\u041D\u043E\u0432\u043E\u0441\u0438\u0431\u0438\u0440\u0441\u043A", nameEn: "Novosibirsk" },
{ countryCode: "RU", name: "\u0415\u043A\u0430\u0442\u0435\u0440\u0438\u043D\u0431\u0443\u0440\u0433", nameEn: "Yekaterinburg" },
{ countryCode: "RU", name: "\u041A\u0430\u0437\u0430\u043D\u044C", nameEn: "Kazan" },
{ countryCode: "RU", name: "\u041D\u0438\u0436\u043D\u0438\u0439 \u041D\u043E\u0432\u0433\u043E\u0440\u043E\u0434", nameEn: "Nizhny Novgorod" },
{ countryCode: "RU", name: "\u0421\u0430\u043C\u0430\u0440\u0430", nameEn: "Samara" },
{ countryCode: "RU", name: "\u0420\u043E\u0441\u0442\u043E\u0432-\u043D\u0430-\u0414\u043E\u043D\u0443", nameEn: "Rostov-on-Don" },
{ countryCode: "RU", name: "\u0423\u0444\u0430", nameEn: "Ufa" },
{ countryCode: "RU", name: "\u041A\u0440\u0430\u0441\u043D\u043E\u0434\u0430\u0440", nameEn: "Krasnodar" },
{ countryCode: "RU", name: "\u0421\u043E\u0447\u0438", nameEn: "Sochi" },
{ countryCode: "RU", name: "\u0412\u043B\u0430\u0434\u0438\u0432\u043E\u0441\u0442\u043E\u043A", nameEn: "Vladivostok" },
{ countryCode: "RU", name: "\u0418\u0440\u043A\u0443\u0442\u0441\u043A", nameEn: "Irkutsk" },
{ countryCode: "RU", name: "\u0425\u0430\u0431\u0430\u0440\u043E\u0432\u0441\u043A", nameEn: "Khabarovsk" },
{ countryCode: "RU", name: "\u0422\u044E\u043C\u0435\u043D\u044C", nameEn: "Tyumen" },
// Нидерланды
{ countryCode: "NL", name: "\u0410\u043C\u0441\u0442\u0435\u0440\u0434\u0430\u043C", nameEn: "Amsterdam" },
{ countryCode: "NL", name: "\u0420\u043E\u0442\u0442\u0435\u0440\u0434\u0430\u043C", nameEn: "Rotterdam" },
{ countryCode: "NL", name: "\u0413\u0430\u0430\u0433\u0430", nameEn: "The Hague" },
// Германия
{ countryCode: "DE", name: "\u0424\u0440\u0430\u043D\u043A\u0444\u0443\u0440\u0442", nameEn: "Frankfurt" },
{ countryCode: "DE", name: "\u0411\u0435\u0440\u043B\u0438\u043D", nameEn: "Berlin" },
{ countryCode: "DE", name: "\u041C\u044E\u043D\u0445\u0435\u043D", nameEn: "Munich" },
{ countryCode: "DE", name: "\u0413\u0430\u043C\u0431\u0443\u0440\u0433", nameEn: "Hamburg" },
{ countryCode: "DE", name: "\u041A\u0451\u043B\u044C\u043D", nameEn: "Cologne" },
{ countryCode: "DE", name: "\u0414\u044E\u0441\u0441\u0435\u043B\u044C\u0434\u043E\u0440\u0444", nameEn: "D\xFCsseldorf" },
{ countryCode: "DE", name: "\u0428\u0442\u0443\u0442\u0433\u0430\u0440\u0442", nameEn: "Stuttgart" },
// Финляндия
{ countryCode: "FI", name: "\u0425\u0435\u043B\u044C\u0441\u0438\u043D\u043A\u0438", nameEn: "Helsinki" },
{ countryCode: "FI", name: "\u042D\u0441\u043F\u043E\u043E", nameEn: "Espoo" },
{ countryCode: "FI", name: "\u0422\u0430\u043C\u043F\u0435\u0440\u0435", nameEn: "Tampere" },
// Швеция
{ countryCode: "SE", name: "\u0421\u0442\u043E\u043A\u0433\u043E\u043B\u044C\u043C", nameEn: "Stockholm" },
{ countryCode: "SE", name: "\u0413\u0451\u0442\u0435\u0431\u043E\u0440\u0433", nameEn: "Gothenburg" },
{ countryCode: "SE", name: "\u041C\u0430\u043B\u044C\u043C\u0451", nameEn: "Malm\xF6" },
// Норвегия
{ countryCode: "NO", name: "\u041E\u0441\u043B\u043E", nameEn: "Oslo" },
{ countryCode: "NO", name: "\u0411\u0435\u0440\u0433\u0435\u043D", nameEn: "Bergen" },
// Дания
{ countryCode: "DK", name: "\u041A\u043E\u043F\u0435\u043D\u0433\u0430\u0433\u0435\u043D", nameEn: "Copenhagen" },
{ countryCode: "DK", name: "\u041E\u0440\u0445\u0443\u0441", nameEn: "Aarhus" },
// Великобритания
{ countryCode: "GB", name: "\u041B\u043E\u043D\u0434\u043E\u043D", nameEn: "London" },
{ countryCode: "GB", name: "\u041C\u0430\u043D\u0447\u0435\u0441\u0442\u0435\u0440", nameEn: "Manchester" },
{ countryCode: "GB", name: "\u0413\u043B\u0430\u0437\u0433\u043E", nameEn: "Glasgow" },
{ countryCode: "GB", name: "\u041A\u0430\u0440\u0434\u0438\u0444\u0444", nameEn: "Cardiff" },
// Франция
{ countryCode: "FR", name: "\u041F\u0430\u0440\u0438\u0436", nameEn: "Paris" },
{ countryCode: "FR", name: "\u041C\u0430\u0440\u0441\u0435\u043B\u044C", nameEn: "Marseille" },
{ countryCode: "FR", name: "\u041B\u0438\u043E\u043D", nameEn: "Lyon" },
{ countryCode: "FR", name: "\u0421\u0442\u0440\u0430\u0441\u0431\u0443\u0440\u0433", nameEn: "Strasbourg" },
// Испания
{ countryCode: "ES", name: "\u041C\u0430\u0434\u0440\u0438\u0434", nameEn: "Madrid" },
{ countryCode: "ES", name: "\u0411\u0430\u0440\u0441\u0435\u043B\u043E\u043D\u0430", nameEn: "Barcelona" },
{ countryCode: "ES", name: "\u0412\u0430\u043B\u0435\u043D\u0441\u0438\u044F", nameEn: "Valencia" },
// Италия
{ countryCode: "IT", name: "\u0420\u0438\u043C", nameEn: "Rome" },
{ countryCode: "IT", name: "\u041C\u0438\u043B\u0430\u043D", nameEn: "Milan" },
{ countryCode: "IT", name: "\u0422\u0443\u0440\u0438\u043D", nameEn: "Turin" },
// Чехия
{ countryCode: "CZ", name: "\u041F\u0440\u0430\u0433\u0430", nameEn: "Prague" },
{ countryCode: "CZ", name: "\u0411\u0440\u043D\u043E", nameEn: "Brno" },
// Польша
{ countryCode: "PL", name: "\u0412\u0430\u0440\u0448\u0430\u0432\u0430", nameEn: "Warsaw" },
{ countryCode: "PL", name: "\u041A\u0440\u0430\u043A\u043E\u0432", nameEn: "Krak\xF3w" },
{ countryCode: "PL", name: "\u0412\u0440\u043E\u0446\u043B\u0430\u0432", nameEn: "Wroc\u0142aw" },
{ countryCode: "PL", name: "\u0413\u0434\u0430\u043D\u044C\u0441\u043A", nameEn: "Gda\u0144sk" },
// Швейцария
{ countryCode: "CH", name: "\u0426\u044E\u0440\u0438\u0445", nameEn: "Zurich" },
{ countryCode: "CH", name: "\u0416\u0435\u043D\u0435\u0432\u0430", nameEn: "Geneva" },
{ countryCode: "CH", name: "\u0411\u0430\u0437\u0435\u043B\u044C", nameEn: "Basel" },
// Бельгия
{ countryCode: "BE", name: "\u0411\u0440\u044E\u0441\u0441\u0435\u043B\u044C", nameEn: "Brussels" },
{ countryCode: "BE", name: "\u0410\u043D\u0442\u0432\u0435\u0440\u043F\u0435\u043D", nameEn: "Antwerp" },
// Австрия
{ countryCode: "AT", name: "\u0412\u0435\u043D\u0430", nameEn: "Vienna" },
{ countryCode: "AT", name: "\u0413\u0440\u0430\u0446", nameEn: "Graz" },
// Латвия
{ countryCode: "LV", name: "\u0420\u0438\u0433\u0430", nameEn: "Riga" },
// Литва
{ countryCode: "LT", name: "\u0412\u0438\u043B\u044C\u043D\u044E\u0441", nameEn: "Vilnius" },
{ countryCode: "LT", name: "\u041A\u0430\u0443\u043D\u0430\u0441", nameEn: "Kaunas" },
// Эстония
{ countryCode: "EE", name: "\u0422\u0430\u043B\u043B\u0438\u043D", nameEn: "Tallinn" },
// Украина
{ countryCode: "UA", name: "\u041A\u0438\u0435\u0432", nameEn: "Kyiv" },
{ countryCode: "UA", name: "\u0425\u0430\u0440\u044C\u043A\u043E\u0432", nameEn: "Kharkiv" },
{ countryCode: "UA", name: "\u041E\u0434\u0435\u0441\u0441\u0430", nameEn: "Odesa" },
{ countryCode: "UA", name: "\u041B\u044C\u0432\u043E\u0432", nameEn: "Lviv" },
// Казахстан
{ countryCode: "KZ", name: "\u0410\u043B\u043C\u0430\u0442\u044B", nameEn: "Almaty" },
{ countryCode: "KZ", name: "\u0410\u0441\u0442\u0430\u043D\u0430", nameEn: "Astana" },
// Беларусь
{ countryCode: "BY", name: "\u041C\u0438\u043D\u0441\u043A", nameEn: "Minsk" },
// США
{ countryCode: "US", name: "\u041D\u044C\u044E-\u0419\u043E\u0440\u043A", nameEn: "New York" },
{ countryCode: "US", name: "\u041B\u043E\u0441-\u0410\u043D\u0434\u0436\u0435\u043B\u0435\u0441", nameEn: "Los Angeles" },
{ countryCode: "US", name: "\u0427\u0438\u043A\u0430\u0433\u043E", nameEn: "Chicago" },
{ countryCode: "US", name: "\u0425\u044C\u044E\u0441\u0442\u043E\u043D", nameEn: "Houston" },
{ countryCode: "US", name: "\u0414\u0430\u043B\u043B\u0430\u0441", nameEn: "Dallas" },
{ countryCode: "US", name: "\u041C\u0430\u0439\u0430\u043C\u0438", nameEn: "Miami" },
{ countryCode: "US", name: "\u0421\u0438\u044D\u0442\u043B", nameEn: "Seattle" },
{ countryCode: "US", name: "\u0421\u0430\u043D-\u0424\u0440\u0430\u043D\u0446\u0438\u0441\u043A\u043E", nameEn: "San Francisco" },
{ countryCode: "US", name: "\u0412\u0430\u0448\u0438\u043D\u0433\u0442\u043E\u043D", nameEn: "Washington" },
{ countryCode: "US", name: "\u0410\u0442\u043B\u0430\u043D\u0442\u0430", nameEn: "Atlanta" },
{ countryCode: "US", name: "\u0414\u0435\u043D\u0432\u0435\u0440", nameEn: "Denver" },
// Канада
{ countryCode: "CA", name: "\u0422\u043E\u0440\u043E\u043D\u0442\u043E", nameEn: "Toronto" },
{ countryCode: "CA", name: "\u041C\u043E\u043D\u0440\u0435\u0430\u043B\u044C", nameEn: "Montreal" },
{ countryCode: "CA", name: "\u0412\u0430\u043D\u043A\u0443\u0432\u0435\u0440", nameEn: "Vancouver" },
// Сингапур
{ countryCode: "SG", name: "\u0421\u0438\u043D\u0433\u0430\u043F\u0443\u0440", nameEn: "Singapore" },
// Япония
{ countryCode: "JP", name: "\u0422\u043E\u043A\u0438\u043E", nameEn: "Tokyo" },
{ countryCode: "JP", name: "\u041E\u0441\u0430\u043A\u0430", nameEn: "Osaka" },
// Южная Корея
{ countryCode: "KR", name: "\u0421\u0435\u0443\u043B", nameEn: "Seoul" },
// Китай
{ countryCode: "CN", name: "\u041F\u0435\u043A\u0438\u043D", nameEn: "Beijing" },
{ countryCode: "CN", name: "\u0428\u0430\u043D\u0445\u0430\u0439", nameEn: "Shanghai" },
{ countryCode: "CN", name: "\u0413\u0443\u0430\u043D\u0447\u0436\u043E\u0443", nameEn: "Guangzhou" },
{ countryCode: "CN", name: "\u0428\u044D\u043D\u044C\u0447\u0436\u044D\u043D\u044C", nameEn: "Shenzhen" },
{ countryCode: "CN", name: "\u0413\u043E\u043D\u043A\u043E\u043D\u0433", nameEn: "Hong Kong" },
// Индия
{ countryCode: "IN", name: "\u041C\u0443\u043C\u0431\u0430\u0438", nameEn: "Mumbai" },
{ countryCode: "IN", name: "\u0414\u0435\u043B\u0438", nameEn: "Delhi" },
{ countryCode: "IN", name: "\u0411\u0430\u043D\u0433\u0430\u043B\u043E\u0440", nameEn: "Bangalore" },
{ countryCode: "IN", name: "\u0427\u0435\u043D\u043D\u0430\u0438", nameEn: "Chennai" },
// ОАЭ
{ countryCode: "AE", name: "\u0414\u0443\u0431\u0430\u0439", nameEn: "Dubai" },
{ countryCode: "AE", name: "\u0410\u0431\u0443-\u0414\u0430\u0431\u0438", nameEn: "Abu Dhabi" },
// Турция
{ countryCode: "TR", name: "\u0421\u0442\u0430\u043C\u0431\u0443\u043B", nameEn: "Istanbul" },
{ countryCode: "TR", name: "\u0410\u043D\u043A\u0430\u0440\u0430", nameEn: "Ankara" },
{ countryCode: "TR", name: "\u0418\u0437\u043C\u0438\u0440", nameEn: "Izmir" },
// Бразилия
{ countryCode: "BR", name: "\u0421\u0430\u043D-\u041F\u0430\u0443\u043B\u0443", nameEn: "S\xE3o Paulo" },
{ countryCode: "BR", name: "\u0420\u0438\u043E-\u0434\u0435-\u0416\u0430\u043D\u0435\u0439\u0440\u043E", nameEn: "Rio de Janeiro" },
// Австралия
{ countryCode: "AU", name: "\u0421\u0438\u0434\u043D\u0435\u0439", nameEn: "Sydney" },
{ countryCode: "AU", name: "\u041C\u0435\u043B\u044C\u0431\u0443\u0440\u043D", nameEn: "Melbourne" },
// Болгария
{ countryCode: "BG", name: "\u0421\u043E\u0444\u0438\u044F", nameEn: "Sofia" },
// Румыния
{ countryCode: "RO", name: "\u0411\u0443\u0445\u0430\u0440\u0435\u0441\u0442", nameEn: "Bucharest" },
// Венгрия
{ countryCode: "HU", name: "\u0411\u0443\u0434\u0430\u043F\u0435\u0448\u0442", nameEn: "Budapest" },
// Греция
{ countryCode: "GR", name: "\u0410\u0444\u0438\u043D\u044B", nameEn: "Athens" },
// Португалия
{ countryCode: "PT", name: "\u041B\u0438\u0441\u0441\u0430\u0431\u043E\u043D", nameEn: "Lisbon" },
// Ирландия
{ countryCode: "IE", name: "\u0414\u0443\u0431\u043B\u0438\u043D", nameEn: "Dublin" },
// Израиль
{ countryCode: "IL", name: "\u0422\u0435\u043B\u044C-\u0410\u0432\u0438\u0432", nameEn: "Tel Aviv" }
];
var CITY_BY_NAME_RU = Object.fromEntries(
CITIES.map((c) => [c.name.toLowerCase(), c])
);
function resolveCityRef(cityName) {
return CITY_BY_NAME_RU[cityName.trim().toLowerCase()];
}
function resolveCountryForCity(cityName) {
const city = resolveCityRef(cityName);
return city ? COUNTRY_BY_CODE[city.countryCode] : void 0;
}
function cityMatchesCountry(cityName, countryName, rows) {
const countryCode = COUNTRY_BY_NAME_RU[countryName.trim().toLowerCase()]?.code;
if (!countryCode) return true;
const catalog = resolveCityRef(cityName);
if (catalog) return catalog.countryCode === countryCode;
return (rows ?? []).some(
(row) => row.city?.trim().toLowerCase() === cityName.trim().toLowerCase() && COUNTRY_BY_NAME_RU[(row.country || "").trim().toLowerCase()]?.code === countryCode
);
}
function buildCityOptions(rows, countryName, options) {
const includeCatalog = options?.includeCatalog ?? true;
const names = /* @__PURE__ */ new Set();
const countryCode = countryName?.trim() ? COUNTRY_BY_NAME_RU[countryName.trim().toLowerCase()]?.code : void 0;
const belongsToCountry = (city, rowCountry) => {
if (!countryCode) return true;
const catalog = resolveCityRef(city);
if (catalog) return catalog.countryCode === countryCode;
if (rowCountry?.trim()) {
return COUNTRY_BY_NAME_RU[rowCountry.trim().toLowerCase()]?.code === countryCode;
}
return false;
};
for (const row of rows ?? []) {
const city = (row.city || "").trim();
if (!city) continue;
if (belongsToCountry(city, row.country ?? void 0)) {
names.add(city);
}
}
if (includeCatalog) {
for (const { name } of listCities(countryCode)) {
names.add(name);
}
}
return [...names].sort((a, b) => a.localeCompare(b, "ru")).map((name) => ({ value: name, label: name }));
}
function resolveCountryForCityFromRows(cityName, rows) {
const trimmed = cityName.trim();
if (!trimmed) return void 0;
const fromCatalog = resolveCountryForCity(trimmed);
if (fromCatalog) return fromCatalog.name;
const row = (rows ?? []).find(
(r) => r.city?.trim().toLowerCase() === trimmed.toLowerCase() && r.country?.trim()
);
return row?.country?.trim();
}
function listCities(countryCode) {
return CITIES.filter((c) => !countryCode || c.countryCode === countryCode).map((c) => ({
...c,
country: COUNTRY_BY_CODE[c.countryCode]
}));
}
function countriesWithCities() {
const codes = new Set(CITIES.map((c) => c.countryCode));
return COUNTRIES.filter((c) => codes.has(c.code));
}
// src/geo/loc-codes.ts
var CANONICAL_LOC_CODES = {
\u043C\u043E\u0441\u043A\u0432\u0430: "msk",
\u0444\u0440\u0430\u043D\u043A\u0444\u0443\u0440\u0442: "fra",
\u0430\u043C\u0441\u0442\u0435\u0440\u0434\u0430\u043C: "ams",
\u0445\u0435\u043B\u044C\u0441\u0438\u043D\u043A\u0438: "hel",
\u043F\u0430\u0440\u0438\u0436: "par",
"\u0441\u0430\u043D\u043A\u0442-\u043F\u0435\u0442\u0435\u0440\u0431\u0443\u0440\u0433": "led",
\u0431\u0435\u0440\u043B\u0438\u043D: "ber",
\u043C\u044E\u043D\u0445\u0435\u043D: "muc",
\u043B\u043E\u043D\u0434\u043E\u043D: "lon",
"\u043D\u044C\u044E-\u0439\u043E\u0440\u043A": "nyc",
\u0442\u043E\u043A\u0438\u043E: "tyo",
\u0441\u0438\u043D\u0433\u0430\u043F\u0443\u0440: "sin",
\u0434\u0443\u0431\u0430\u0439: "dxb",
\u0432\u0430\u0440\u0448\u0430\u0432\u0430: "waw",
\u043F\u0440\u0430\u0433\u0430: "prg",
\u0432\u0435\u043D\u0430: "vie",
\u0440\u0438\u0433\u0430: "rix",
\u0442\u0430\u043B\u043B\u0438\u043D: "tll",
\u043C\u0438\u043D\u0441\u043A: "msq",
\u043A\u0438\u0435\u0432: "iev",
\u0441\u0442\u043E\u043A\u0433\u043E\u043B\u044C\u043C: "arn",
\u043E\u0441\u043B\u043E: "osl",
\u043A\u043E\u043F\u0435\u043D\u0433\u0430\u0433\u0435\u043D: "cph",
\u0446\u044E\u0440\u0438\u0445: "zrh",
\u0431\u0440\u044E\u0441\u0441\u0435\u043B\u044C: "bru",
\u043C\u0430\u0434\u0440\u0438\u0434: "mad",
\u0431\u0430\u0440\u0441\u0435\u043B\u043E\u043D\u0430: "bcn",
\u043C\u0438\u043B\u0430\u043D: "mxp",
\u0440\u0438\u043C: "rom"
};
function locCodeForCity(city, usedCodes) {
const known = CANONICAL_LOC_CODES[city.name.trim().toLowerCase()];
if (known && !usedCodes.has(known)) return known;
if (known) {
}
const base = city.nameEn.toLowerCase().replace(/[^a-z]/g, "");
if (!base) {
return `${city.countryCode.toLowerCase()}${usedCodes.size}`;
}
for (let len = 3; len <= Math.max(3, base.length); len++) {
const candidate = base.slice(0, len);
if (!usedCodes.has(candidate)) return candidate;
}
let i = 2;
while (usedCodes.has(`${base.slice(0, 2)}${i}`)) i += 1;
return `${base.slice(0, 2)}${i}`;
}
function catalogCitiesWithLocCodes() {
const used = /* @__PURE__ */ new Set();
const ordered = [...CITIES].sort((a, b) => {
const ak = CANONICAL_LOC_CODES[a.name.toLowerCase()] ? 0 : 1;
const bk = CANONICAL_LOC_CODES[b.name.toLowerCase()] ? 0 : 1;
return ak - bk;
});
return ordered.map((city) => {
const locCode = locCodeForCity(city, used);
used.add(locCode);
return { ...city, locCode };
});
}
export {
COUNTRIES,
COUNTRY_BY_CODE,
COUNTRY_BY_NAME_RU,
CITIES,
CITY_BY_NAME_RU,
resolveCityRef,
resolveCountryForCity,
cityMatchesCountry,
buildCityOptions,
resolveCountryForCityFromRows,
listCities,
countriesWithCities,
CANONICAL_LOC_CODES,
locCodeForCity,
catalogCitiesWithLocCodes
};
+76
View File
@@ -0,0 +1,76 @@
/**
* Стандартизированный справочник стран (ISO 3166-1 alpha-2) с русскими названиями.
* Источник: общедоступный справочник. Не зависит от данных в БД vps-tracker.
*/
interface CountryRef {
/** ISO 3166-1 alpha-2 код */
code: string;
/** Русское название */
name: string;
/** Английское название */
nameEn: string;
}
declare const COUNTRIES: readonly CountryRef[];
declare const COUNTRY_BY_CODE: Record<string, CountryRef>;
declare const COUNTRY_BY_NAME_RU: Record<string, CountryRef>;
/**
* Стандартизированный справочник городов по странам.
* Используется в фильтрах VPS и форме редактирования.
* Не зависит от данных в БД vps-tracker — общедоступный статичный справочник.
*/
interface CityRef {
/** ISO 3166-1 alpha-2 код страны */
countryCode: string;
/** Русское название города */
name: string;
/** Английское название города */
nameEn: string;
}
/**
* Топ-города по странам (включая города с крупными дата-центрами).
* Список намеренно ограничен — расширяется по мере необходимости.
*/
declare const CITIES: readonly CityRef[];
declare const CITY_BY_NAME_RU: Record<string, CityRef>;
/** Найти город в справочнике по русскому названию. */
declare function resolveCityRef(cityName: string): CityRef | undefined;
/** Страна из справочника для города (если есть привязка). */
declare function resolveCountryForCity(cityName: string): CountryRef | undefined;
interface CityLocationRow {
city?: string | null;
country?: string | null;
}
/** Проверить, относится ли город к выбранной стране (справочник или данные VPS). */
declare function cityMatchesCountry(cityName: string, countryName: string, rows?: readonly CityLocationRow[]): boolean;
/** Опции городов: из VPS и опционально справочника, опционально по стране. */
declare function buildCityOptions(rows: readonly CityLocationRow[] | undefined, countryName?: string, options?: {
includeCatalog?: boolean;
}): {
value: string;
label: string;
}[];
/** Страна для города: справочник, иначе первая запись VPS с таким городом. */
declare function resolveCountryForCityFromRows(cityName: string, rows: readonly CityLocationRow[] | undefined): string | undefined;
interface CityOption extends CityRef {
country: CountryRef;
}
/** Список городов с привязкой к стране. */
declare function listCities(countryCode?: string): CityOption[];
/** Список стран, у которых есть города в справочнике. */
declare function countriesWithCities(): CountryRef[];
/**
* Канонические коды {loc} для hostname (из naming seed / docs).
* Ключ — русское имя города (lower case).
*/
declare const CANONICAL_LOC_CODES: Record<string, string>;
/** Детерминированный уникальный loc-код для города (hostname {loc}). */
declare function locCodeForCity(city: Pick<CityRef, 'name' | 'nameEn' | 'countryCode'>, usedCodes: ReadonlySet<string>): string;
/** Все города каталога с выделенными loc-кодами (без коллизий). */
declare function catalogCitiesWithLocCodes(): Array<CityRef & {
locCode: string;
}>;
export { CANONICAL_LOC_CODES, CITIES, CITY_BY_NAME_RU, COUNTRIES, COUNTRY_BY_CODE, COUNTRY_BY_NAME_RU, type CityLocationRow, type CityOption, type CityRef, type CountryRef, buildCityOptions, catalogCitiesWithLocCodes, cityMatchesCountry, countriesWithCities, listCities, locCodeForCity, resolveCityRef, resolveCountryForCity, resolveCountryForCityFromRows };
+34
View File
@@ -0,0 +1,34 @@
import {
CANONICAL_LOC_CODES,
CITIES,
CITY_BY_NAME_RU,
COUNTRIES,
COUNTRY_BY_CODE,
COUNTRY_BY_NAME_RU,
buildCityOptions,
catalogCitiesWithLocCodes,
cityMatchesCountry,
countriesWithCities,
listCities,
locCodeForCity,
resolveCityRef,
resolveCountryForCity,
resolveCountryForCityFromRows
} from "../chunk-FK3U7DSV.js";
export {
CANONICAL_LOC_CODES,
CITIES,
CITY_BY_NAME_RU,
COUNTRIES,
COUNTRY_BY_CODE,
COUNTRY_BY_NAME_RU,
buildCityOptions,
catalogCitiesWithLocCodes,
cityMatchesCountry,
countriesWithCities,
listCities,
locCodeForCity,
resolveCityRef,
resolveCountryForCity,
resolveCountryForCityFromRows
};
+1
View File
@@ -1,4 +1,5 @@
import { z } from 'zod';
export { CANONICAL_LOC_CODES, CITIES, CITY_BY_NAME_RU, COUNTRIES, COUNTRY_BY_CODE, COUNTRY_BY_NAME_RU, CityLocationRow, CityOption, CityRef, CountryRef, buildCityOptions, catalogCitiesWithLocCodes, cityMatchesCountry, countriesWithCities, listCities, locCodeForCity, resolveCityRef, resolveCountryForCity, resolveCountryForCityFromRows } from './geo/index.js';
declare const appSwitcherIconSchema: z.ZodEnum<{
server: "server";
+33
View File
@@ -1,3 +1,21 @@
import {
CANONICAL_LOC_CODES,
CITIES,
CITY_BY_NAME_RU,
COUNTRIES,
COUNTRY_BY_CODE,
COUNTRY_BY_NAME_RU,
buildCityOptions,
catalogCitiesWithLocCodes,
cityMatchesCountry,
countriesWithCities,
listCities,
locCodeForCity,
resolveCityRef,
resolveCountryForCity,
resolveCountryForCityFromRows
} from "./chunk-FK3U7DSV.js";
// src/app-switcher.ts
import { z } from "zod";
var appSwitcherIconSchema = z.enum([
@@ -265,6 +283,12 @@ var ValidationError = class extends Error {
}
};
export {
CANONICAL_LOC_CODES,
CITIES,
CITY_BY_NAME_RU,
COUNTRIES,
COUNTRY_BY_CODE,
COUNTRY_BY_NAME_RU,
ValidationError,
addressFamilySchema,
aliasCreateSchema,
@@ -279,10 +303,16 @@ export {
appSwitcherEntrySchema,
appSwitcherIconSchema,
bindExportSchema,
buildCityOptions,
catalogCitiesWithLocCodes,
cfDnsRecordSchema,
cfZoneSchema,
cityMatchesCountry,
countriesWithCities,
createDnsRecordPayloadSchema,
dashboardStatsSchema,
listCities,
locCodeForCity,
locationSchema,
loginSchema,
nodeAddressSchema,
@@ -292,6 +322,9 @@ export {
nodeSchema,
orphanIgnoreSchema,
patchDnsRecordPayloadSchema,
resolveCityRef,
resolveCountryForCity,
resolveCountryForCityFromRows,
syncApplySchema,
syncDiffOpSchema,
syncJobSchema,
+6 -1
View File
@@ -12,10 +12,15 @@
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"import": "./dist/index.js"
},
"./geo": {
"types": "./dist/geo/index.d.ts",
"development": "./src/geo/index.ts",
"import": "./dist/geo/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"build": "tsup src/index.ts src/geo/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --dts --watch",
"test": "vitest run --passWithNoTests"
},
+294
View File
@@ -0,0 +1,294 @@
/**
* Стандартизированный справочник городов по странам.
* Используется в фильтрах VPS и форме редактирования.
* Не зависит от данных в БД vps-tracker — общедоступный статичный справочник.
*/
import { COUNTRY_BY_CODE, COUNTRY_BY_NAME_RU, COUNTRIES, type CountryRef } from './countries.js'
export interface CityRef {
/** ISO 3166-1 alpha-2 код страны */
countryCode: string
/** Русское название города */
name: string
/** Английское название города */
nameEn: string
}
/**
* Топ-города по странам (включая города с крупными дата-центрами).
* Список намеренно ограничен — расширяется по мере необходимости.
*/
export const CITIES: readonly CityRef[] = [
// Россия
{ countryCode: 'RU', name: 'Москва', nameEn: 'Moscow' },
{ countryCode: 'RU', name: 'Санкт-Петербург', nameEn: 'Saint Petersburg' },
{ countryCode: 'RU', name: 'Новосибирск', nameEn: 'Novosibirsk' },
{ countryCode: 'RU', name: 'Екатеринбург', nameEn: 'Yekaterinburg' },
{ countryCode: 'RU', name: 'Казань', nameEn: 'Kazan' },
{ countryCode: 'RU', name: 'Нижний Новгород', nameEn: 'Nizhny Novgorod' },
{ countryCode: 'RU', name: 'Самара', nameEn: 'Samara' },
{ countryCode: 'RU', name: 'Ростов-на-Дону', nameEn: 'Rostov-on-Don' },
{ countryCode: 'RU', name: 'Уфа', nameEn: 'Ufa' },
{ countryCode: 'RU', name: 'Краснодар', nameEn: 'Krasnodar' },
{ countryCode: 'RU', name: 'Сочи', nameEn: 'Sochi' },
{ countryCode: 'RU', name: 'Владивосток', nameEn: 'Vladivostok' },
{ countryCode: 'RU', name: 'Иркутск', nameEn: 'Irkutsk' },
{ countryCode: 'RU', name: 'Хабаровск', nameEn: 'Khabarovsk' },
{ countryCode: 'RU', name: 'Тюмень', nameEn: 'Tyumen' },
// Нидерланды
{ countryCode: 'NL', name: 'Амстердам', nameEn: 'Amsterdam' },
{ countryCode: 'NL', name: 'Роттердам', nameEn: 'Rotterdam' },
{ countryCode: 'NL', name: 'Гаага', nameEn: 'The Hague' },
// Германия
{ countryCode: 'DE', name: 'Франкфурт', nameEn: 'Frankfurt' },
{ countryCode: 'DE', name: 'Берлин', nameEn: 'Berlin' },
{ countryCode: 'DE', name: 'Мюнхен', nameEn: 'Munich' },
{ countryCode: 'DE', name: 'Гамбург', nameEn: 'Hamburg' },
{ countryCode: 'DE', name: 'Кёльн', nameEn: 'Cologne' },
{ countryCode: 'DE', name: 'Дюссельдорф', nameEn: 'Düsseldorf' },
{ countryCode: 'DE', name: 'Штутгарт', nameEn: 'Stuttgart' },
// Финляндия
{ countryCode: 'FI', name: 'Хельсинки', nameEn: 'Helsinki' },
{ countryCode: 'FI', name: 'Эспоо', nameEn: 'Espoo' },
{ countryCode: 'FI', name: 'Тампере', nameEn: 'Tampere' },
// Швеция
{ countryCode: 'SE', name: 'Стокгольм', nameEn: 'Stockholm' },
{ countryCode: 'SE', name: 'Гётеборг', nameEn: 'Gothenburg' },
{ countryCode: 'SE', name: 'Мальмё', nameEn: 'Malmö' },
// Норвегия
{ countryCode: 'NO', name: 'Осло', nameEn: 'Oslo' },
{ countryCode: 'NO', name: 'Берген', nameEn: 'Bergen' },
// Дания
{ countryCode: 'DK', name: 'Копенгаген', nameEn: 'Copenhagen' },
{ countryCode: 'DK', name: 'Орхус', nameEn: 'Aarhus' },
// Великобритания
{ countryCode: 'GB', name: 'Лондон', nameEn: 'London' },
{ countryCode: 'GB', name: 'Манчестер', nameEn: 'Manchester' },
{ countryCode: 'GB', name: 'Глазго', nameEn: 'Glasgow' },
{ countryCode: 'GB', name: 'Кардифф', nameEn: 'Cardiff' },
// Франция
{ countryCode: 'FR', name: 'Париж', nameEn: 'Paris' },
{ countryCode: 'FR', name: 'Марсель', nameEn: 'Marseille' },
{ countryCode: 'FR', name: 'Лион', nameEn: 'Lyon' },
{ countryCode: 'FR', name: 'Страсбург', nameEn: 'Strasbourg' },
// Испания
{ countryCode: 'ES', name: 'Мадрид', nameEn: 'Madrid' },
{ countryCode: 'ES', name: 'Барселона', nameEn: 'Barcelona' },
{ countryCode: 'ES', name: 'Валенсия', nameEn: 'Valencia' },
// Италия
{ countryCode: 'IT', name: 'Рим', nameEn: 'Rome' },
{ countryCode: 'IT', name: 'Милан', nameEn: 'Milan' },
{ countryCode: 'IT', name: 'Турин', nameEn: 'Turin' },
// Чехия
{ countryCode: 'CZ', name: 'Прага', nameEn: 'Prague' },
{ countryCode: 'CZ', name: 'Брно', nameEn: 'Brno' },
// Польша
{ countryCode: 'PL', name: 'Варшава', nameEn: 'Warsaw' },
{ countryCode: 'PL', name: 'Краков', nameEn: 'Kraków' },
{ countryCode: 'PL', name: 'Вроцлав', nameEn: 'Wrocław' },
{ countryCode: 'PL', name: 'Гданьск', nameEn: 'Gdańsk' },
// Швейцария
{ countryCode: 'CH', name: 'Цюрих', nameEn: 'Zurich' },
{ countryCode: 'CH', name: 'Женева', nameEn: 'Geneva' },
{ countryCode: 'CH', name: 'Базель', nameEn: 'Basel' },
// Бельгия
{ countryCode: 'BE', name: 'Брюссель', nameEn: 'Brussels' },
{ countryCode: 'BE', name: 'Антверпен', nameEn: 'Antwerp' },
// Австрия
{ countryCode: 'AT', name: 'Вена', nameEn: 'Vienna' },
{ countryCode: 'AT', name: 'Грац', nameEn: 'Graz' },
// Латвия
{ countryCode: 'LV', name: 'Рига', nameEn: 'Riga' },
// Литва
{ countryCode: 'LT', name: 'Вильнюс', nameEn: 'Vilnius' },
{ countryCode: 'LT', name: 'Каунас', nameEn: 'Kaunas' },
// Эстония
{ countryCode: 'EE', name: 'Таллин', nameEn: 'Tallinn' },
// Украина
{ countryCode: 'UA', name: 'Киев', nameEn: 'Kyiv' },
{ countryCode: 'UA', name: 'Харьков', nameEn: 'Kharkiv' },
{ countryCode: 'UA', name: 'Одесса', nameEn: 'Odesa' },
{ countryCode: 'UA', name: 'Львов', nameEn: 'Lviv' },
// Казахстан
{ countryCode: 'KZ', name: 'Алматы', nameEn: 'Almaty' },
{ countryCode: 'KZ', name: 'Астана', nameEn: 'Astana' },
// Беларусь
{ countryCode: 'BY', name: 'Минск', nameEn: 'Minsk' },
// США
{ countryCode: 'US', name: 'Нью-Йорк', nameEn: 'New York' },
{ countryCode: 'US', name: 'Лос-Анджелес', nameEn: 'Los Angeles' },
{ countryCode: 'US', name: 'Чикаго', nameEn: 'Chicago' },
{ countryCode: 'US', name: 'Хьюстон', nameEn: 'Houston' },
{ countryCode: 'US', name: 'Даллас', nameEn: 'Dallas' },
{ countryCode: 'US', name: 'Майами', nameEn: 'Miami' },
{ countryCode: 'US', name: 'Сиэтл', nameEn: 'Seattle' },
{ countryCode: 'US', name: 'Сан-Франциско', nameEn: 'San Francisco' },
{ countryCode: 'US', name: 'Вашингтон', nameEn: 'Washington' },
{ countryCode: 'US', name: 'Атланта', nameEn: 'Atlanta' },
{ countryCode: 'US', name: 'Денвер', nameEn: 'Denver' },
// Канада
{ countryCode: 'CA', name: 'Торонто', nameEn: 'Toronto' },
{ countryCode: 'CA', name: 'Монреаль', nameEn: 'Montreal' },
{ countryCode: 'CA', name: 'Ванкувер', nameEn: 'Vancouver' },
// Сингапур
{ countryCode: 'SG', name: 'Сингапур', nameEn: 'Singapore' },
// Япония
{ countryCode: 'JP', name: 'Токио', nameEn: 'Tokyo' },
{ countryCode: 'JP', name: 'Осака', nameEn: 'Osaka' },
// Южная Корея
{ countryCode: 'KR', name: 'Сеул', nameEn: 'Seoul' },
// Китай
{ countryCode: 'CN', name: 'Пекин', nameEn: 'Beijing' },
{ countryCode: 'CN', name: 'Шанхай', nameEn: 'Shanghai' },
{ countryCode: 'CN', name: 'Гуанчжоу', nameEn: 'Guangzhou' },
{ countryCode: 'CN', name: 'Шэньчжэнь', nameEn: 'Shenzhen' },
{ countryCode: 'CN', name: 'Гонконг', nameEn: 'Hong Kong' },
// Индия
{ countryCode: 'IN', name: 'Мумбаи', nameEn: 'Mumbai' },
{ countryCode: 'IN', name: 'Дели', nameEn: 'Delhi' },
{ countryCode: 'IN', name: 'Бангалор', nameEn: 'Bangalore' },
{ countryCode: 'IN', name: 'Ченнаи', nameEn: 'Chennai' },
// ОАЭ
{ countryCode: 'AE', name: 'Дубай', nameEn: 'Dubai' },
{ countryCode: 'AE', name: 'Абу-Даби', nameEn: 'Abu Dhabi' },
// Турция
{ countryCode: 'TR', name: 'Стамбул', nameEn: 'Istanbul' },
{ countryCode: 'TR', name: 'Анкара', nameEn: 'Ankara' },
{ countryCode: 'TR', name: 'Измир', nameEn: 'Izmir' },
// Бразилия
{ countryCode: 'BR', name: 'Сан-Паулу', nameEn: 'São Paulo' },
{ countryCode: 'BR', name: 'Рио-де-Жанейро', nameEn: 'Rio de Janeiro' },
// Австралия
{ countryCode: 'AU', name: 'Сидней', nameEn: 'Sydney' },
{ countryCode: 'AU', name: 'Мельбурн', nameEn: 'Melbourne' },
// Болгария
{ countryCode: 'BG', name: 'София', nameEn: 'Sofia' },
// Румыния
{ countryCode: 'RO', name: 'Бухарест', nameEn: 'Bucharest' },
// Венгрия
{ countryCode: 'HU', name: 'Будапешт', nameEn: 'Budapest' },
// Греция
{ countryCode: 'GR', name: 'Афины', nameEn: 'Athens' },
// Португалия
{ countryCode: 'PT', name: 'Лиссабон', nameEn: 'Lisbon' },
// Ирландия
{ countryCode: 'IE', name: 'Дублин', nameEn: 'Dublin' },
// Израиль
{ countryCode: 'IL', name: 'Тель-Авив', nameEn: 'Tel Aviv' },
]
export const CITY_BY_NAME_RU: Record<string, CityRef> = Object.fromEntries(
CITIES.map((c) => [c.name.toLowerCase(), c]),
)
/** Найти город в справочнике по русскому названию. */
export function resolveCityRef(cityName: string): CityRef | undefined {
return CITY_BY_NAME_RU[cityName.trim().toLowerCase()]
}
/** Страна из справочника для города (если есть привязка). */
export function resolveCountryForCity(cityName: string): CountryRef | undefined {
const city = resolveCityRef(cityName)
return city ? COUNTRY_BY_CODE[city.countryCode] : undefined
}
export interface CityLocationRow {
city?: string | null
country?: string | null
}
/** Проверить, относится ли город к выбранной стране (справочник или данные VPS). */
export function cityMatchesCountry(
cityName: string,
countryName: string,
rows?: readonly CityLocationRow[],
): boolean {
const countryCode = COUNTRY_BY_NAME_RU[countryName.trim().toLowerCase()]?.code
if (!countryCode) return true
const catalog = resolveCityRef(cityName)
if (catalog) return catalog.countryCode === countryCode
return (rows ?? []).some(
(row) =>
row.city?.trim().toLowerCase() === cityName.trim().toLowerCase() &&
COUNTRY_BY_NAME_RU[(row.country || '').trim().toLowerCase()]?.code === countryCode,
)
}
/** Опции городов: из VPS и опционально справочника, опционально по стране. */
export function buildCityOptions(
rows: readonly CityLocationRow[] | undefined,
countryName?: string,
options?: { includeCatalog?: boolean },
): { value: string; label: string }[] {
const includeCatalog = options?.includeCatalog ?? true
const names = new Set<string>()
const countryCode = countryName?.trim()
? COUNTRY_BY_NAME_RU[countryName.trim().toLowerCase()]?.code
: undefined
const belongsToCountry = (city: string, rowCountry?: string): boolean => {
if (!countryCode) return true
const catalog = resolveCityRef(city)
if (catalog) return catalog.countryCode === countryCode
if (rowCountry?.trim()) {
return COUNTRY_BY_NAME_RU[rowCountry.trim().toLowerCase()]?.code === countryCode
}
return false
}
for (const row of rows ?? []) {
const city = (row.city || '').trim()
if (!city) continue
if (belongsToCountry(city, row.country ?? undefined)) {
names.add(city)
}
}
if (includeCatalog) {
for (const { name } of listCities(countryCode)) {
names.add(name)
}
}
return [...names]
.sort((a, b) => a.localeCompare(b, 'ru'))
.map((name) => ({ value: name, label: name }))
}
/** Страна для города: справочник, иначе первая запись VPS с таким городом. */
export function resolveCountryForCityFromRows(
cityName: string,
rows: readonly CityLocationRow[] | undefined,
): string | undefined {
const trimmed = cityName.trim()
if (!trimmed) return undefined
const fromCatalog = resolveCountryForCity(trimmed)
if (fromCatalog) return fromCatalog.name
const row = (rows ?? []).find(
(r) => r.city?.trim().toLowerCase() === trimmed.toLowerCase() && r.country?.trim(),
)
return row?.country?.trim()
}
export interface CityOption extends CityRef {
country: CountryRef
}
/** Список городов с привязкой к стране. */
export function listCities(countryCode?: string): CityOption[] {
return CITIES.filter((c) => !countryCode || c.countryCode === countryCode).map((c) => ({
...c,
country: COUNTRY_BY_CODE[c.countryCode],
}))
}
/** Список стран, у которых есть города в справочнике. */
export function countriesWithCities(): CountryRef[] {
const codes = new Set(CITIES.map((c) => c.countryCode))
return COUNTRIES.filter((c) => codes.has(c.code))
}
+175
View File
@@ -0,0 +1,175 @@
/**
* Стандартизированный справочник стран (ISO 3166-1 alpha-2) с русскими названиями.
* Источник: общедоступный справочник. Не зависит от данных в БД vps-tracker.
*/
export interface CountryRef {
/** ISO 3166-1 alpha-2 код */
code: string
/** Русское название */
name: string
/** Английское название */
nameEn: string
}
export const COUNTRIES: readonly CountryRef[] = [
{ code: 'AU', name: 'Австралия', nameEn: 'Australia' },
{ code: 'AT', name: 'Австрия', nameEn: 'Austria' },
{ code: 'AZ', name: 'Азербайджан', nameEn: 'Azerbaijan' },
{ code: 'AL', name: 'Албания', nameEn: 'Albania' },
{ code: 'DZ', name: 'Алжир', nameEn: 'Algeria' },
{ code: 'AO', name: 'Ангола', nameEn: 'Angola' },
{ code: 'AR', name: 'Аргентина', nameEn: 'Argentina' },
{ code: 'AM', name: 'Армения', nameEn: 'Armenia' },
{ code: 'AF', name: 'Афганистан', nameEn: 'Afghanistan' },
{ code: 'BE', name: 'Бельгия', nameEn: 'Belgium' },
{ code: 'BG', name: 'Болгария', nameEn: 'Bulgaria' },
{ code: 'BO', name: 'Боливия', nameEn: 'Bolivia' },
{ code: 'BA', name: 'Босния и Герцеговина', nameEn: 'Bosnia and Herzegovina' },
{ code: 'BR', name: 'Бразилия', nameEn: 'Brazil' },
{ code: 'GB', name: 'Великобритания', nameEn: 'United Kingdom' },
{ code: 'HU', name: 'Венгрия', nameEn: 'Hungary' },
{ code: 'VE', name: 'Венесуэла', nameEn: 'Venezuela' },
{ code: 'VN', name: 'Вьетнам', nameEn: 'Vietnam' },
{ code: 'GA', name: 'Габон', nameEn: 'Gabon' },
{ code: 'HT', name: 'Гаити', nameEn: 'Haiti' },
{ code: 'GY', name: 'Гайана', nameEn: 'Guyana' },
{ code: 'GM', name: 'Гамбия', nameEn: 'Gambia' },
{ code: 'GH', name: 'Гана', nameEn: 'Ghana' },
{ code: 'GT', name: 'Гватемала', nameEn: 'Guatemala' },
{ code: 'GN', name: 'Гвинея', nameEn: 'Guinea' },
{ code: 'DE', name: 'Германия', nameEn: 'Germany' },
{ code: 'HN', name: 'Гондурас', nameEn: 'Honduras' },
{ code: 'GR', name: 'Греция', nameEn: 'Greece' },
{ code: 'GE', name: 'Грузия', nameEn: 'Georgia' },
{ code: 'DK', name: 'Дания', nameEn: 'Denmark' },
{ code: 'CD', name: 'ДР Конго', nameEn: 'DR Congo' },
{ code: 'EG', name: 'Египет', nameEn: 'Egypt' },
{ code: 'ZM', name: 'Замбия', nameEn: 'Zambia' },
{ code: 'ZW', name: 'Зимбабве', nameEn: 'Zimbabwe' },
{ code: 'IL', name: 'Израиль', nameEn: 'Israel' },
{ code: 'IN', name: 'Индия', nameEn: 'India' },
{ code: 'ID', name: 'Индонезия', nameEn: 'Indonesia' },
{ code: 'JO', name: 'Иордания', nameEn: 'Jordan' },
{ code: 'IQ', name: 'Ирак', nameEn: 'Iraq' },
{ code: 'IR', name: 'Иран', nameEn: 'Iran' },
{ code: 'IE', name: 'Ирландия', nameEn: 'Ireland' },
{ code: 'IS', name: 'Исландия', nameEn: 'Iceland' },
{ code: 'ES', name: 'Испания', nameEn: 'Spain' },
{ code: 'IT', name: 'Италия', nameEn: 'Italy' },
{ code: 'YE', name: 'Йемен', nameEn: 'Yemen' },
{ code: 'KZ', name: 'Казахстан', nameEn: 'Kazakhstan' },
{ code: 'KH', name: 'Камбоджа', nameEn: 'Cambodia' },
{ code: 'CM', name: 'Камерун', nameEn: 'Cameroon' },
{ code: 'CA', name: 'Канада', nameEn: 'Canada' },
{ code: 'QA', name: 'Катар', nameEn: 'Qatar' },
{ code: 'KE', name: 'Кения', nameEn: 'Kenya' },
{ code: 'CY', name: 'Кипр', nameEn: 'Cyprus' },
{ code: 'KG', name: 'Киргизия', nameEn: 'Kyrgyzstan' },
{ code: 'CN', name: 'Китай', nameEn: 'China' },
{ code: 'CO', name: 'Колумбия', nameEn: 'Colombia' },
{ code: 'CG', name: 'Конго', nameEn: 'Congo' },
{ code: 'KR', name: 'Южная Корея', nameEn: 'South Korea' },
{ code: 'KP', name: 'Северная Корея', nameEn: 'North Korea' },
{ code: 'XK', name: 'Косово', nameEn: 'Kosovo' },
{ code: 'CR', name: 'Коста-Рика', nameEn: 'Costa Rica' },
{ code: 'CI', name: "Кот-д'Ивуар", nameEn: "Côte d'Ivoire" },
{ code: 'CU', name: 'Куба', nameEn: 'Cuba' },
{ code: 'KW', name: 'Кувейт', nameEn: 'Kuwait' },
{ code: 'LA', name: 'Лаос', nameEn: 'Laos' },
{ code: 'LV', name: 'Латвия', nameEn: 'Latvia' },
{ code: 'LR', name: 'Либерия', nameEn: 'Liberia' },
{ code: 'LB', name: 'Ливан', nameEn: 'Lebanon' },
{ code: 'LY', name: 'Ливия', nameEn: 'Libya' },
{ code: 'LT', name: 'Литва', nameEn: 'Lithuania' },
{ code: 'LI', name: 'Лихтенштейн', nameEn: 'Liechtenstein' },
{ code: 'LU', name: 'Люксембург', nameEn: 'Luxembourg' },
{ code: 'MU', name: 'Маврикий', nameEn: 'Mauritius' },
{ code: 'MR', name: 'Мавритания', nameEn: 'Mauritania' },
{ code: 'MG', name: 'Мадагаскар', nameEn: 'Madagascar' },
{ code: 'MO', name: 'Макао', nameEn: 'Macao' },
{ code: 'MW', name: 'Малави', nameEn: 'Malawi' },
{ code: 'MY', name: 'Малайзия', nameEn: 'Malaysia' },
{ code: 'ML', name: 'Мали', nameEn: 'Mali' },
{ code: 'MT', name: 'Мальта', nameEn: 'Malta' },
{ code: 'MA', name: 'Марокко', nameEn: 'Morocco' },
{ code: 'MX', name: 'Мексика', nameEn: 'Mexico' },
{ code: 'MZ', name: 'Мозамбик', nameEn: 'Mozambique' },
{ code: 'MD', name: 'Молдавия', nameEn: 'Moldova' },
{ code: 'MC', name: 'Монако', nameEn: 'Monaco' },
{ code: 'MN', name: 'Монголия', nameEn: 'Mongolia' },
{ code: 'MM', name: 'Мьянма', nameEn: 'Myanmar' },
{ code: 'NA', name: 'Намибия', nameEn: 'Namibia' },
{ code: 'NP', name: 'Непал', nameEn: 'Nepal' },
{ code: 'NE', name: 'Нигер', nameEn: 'Niger' },
{ code: 'NG', name: 'Нигерия', nameEn: 'Nigeria' },
{ code: 'NL', name: 'Нидерланды', nameEn: 'Netherlands' },
{ code: 'NI', name: 'Никарагуа', nameEn: 'Nicaragua' },
{ code: 'NZ', name: 'Новая Зеландия', nameEn: 'New Zealand' },
{ code: 'NO', name: 'Норвегия', nameEn: 'Norway' },
{ code: 'AE', name: 'ОАЭ', nameEn: 'United Arab Emirates' },
{ code: 'OM', name: 'Оман', nameEn: 'Oman' },
{ code: 'PK', name: 'Пакистан', nameEn: 'Pakistan' },
{ code: 'PA', name: 'Панама', nameEn: 'Panama' },
{ code: 'PG', name: 'Папуа — Новая Гвинея', nameEn: 'Papua New Guinea' },
{ code: 'PY', name: 'Парагвай', nameEn: 'Paraguay' },
{ code: 'PE', name: 'Перу', nameEn: 'Peru' },
{ code: 'PL', name: 'Польша', nameEn: 'Poland' },
{ code: 'PT', name: 'Португалия', nameEn: 'Portugal' },
{ code: 'RU', name: 'Россия', nameEn: 'Russia' },
{ code: 'RW', name: 'Руанда', nameEn: 'Rwanda' },
{ code: 'RO', name: 'Румыния', nameEn: 'Romania' },
{ code: 'SV', name: 'Сальвадор', nameEn: 'El Salvador' },
{ code: 'SA', name: 'Саудовская Аравия', nameEn: 'Saudi Arabia' },
{ code: 'MK', name: 'Северная Македония', nameEn: 'North Macedonia' },
{ code: 'SC', name: 'Сейшелы', nameEn: 'Seychelles' },
{ code: 'SN', name: 'Сенегал', nameEn: 'Senegal' },
{ code: 'RS', name: 'Сербия', nameEn: 'Serbia' },
{ code: 'SG', name: 'Сингапур', nameEn: 'Singapore' },
{ code: 'SK', name: 'Словакия', nameEn: 'Slovakia' },
{ code: 'SI', name: 'Словения', nameEn: 'Slovenia' },
{ code: 'SO', name: 'Сомали', nameEn: 'Somalia' },
{ code: 'SD', name: 'Судан', nameEn: 'Sudan' },
{ code: 'US', name: 'США', nameEn: 'United States' },
{ code: 'TJ', name: 'Таджикистан', nameEn: 'Tajikistan' },
{ code: 'TH', name: 'Таиланд', nameEn: 'Thailand' },
{ code: 'TZ', name: 'Танзания', nameEn: 'Tanzania' },
{ code: 'TG', name: 'Того', nameEn: 'Togo' },
{ code: 'TT', name: 'Тринидад и Тобаго', nameEn: 'Trinidad and Tobago' },
{ code: 'TV', name: 'Тувалу', nameEn: 'Tuvalu' },
{ code: 'TN', name: 'Тунис', nameEn: 'Tunisia' },
{ code: 'TM', name: 'Туркмения', nameEn: 'Turkmenistan' },
{ code: 'TR', name: 'Турция', nameEn: 'Turkey' },
{ code: 'UG', name: 'Уганда', nameEn: 'Uganda' },
{ code: 'UZ', name: 'Узбекистан', nameEn: 'Uzbekistan' },
{ code: 'UA', name: 'Украина', nameEn: 'Ukraine' },
{ code: 'UY', name: 'Уругвай', nameEn: 'Uruguay' },
{ code: 'FJ', name: 'Фиджи', nameEn: 'Fiji' },
{ code: 'PH', name: 'Филиппины', nameEn: 'Philippines' },
{ code: 'FI', name: 'Финляндия', nameEn: 'Finland' },
{ code: 'FR', name: 'Франция', nameEn: 'France' },
{ code: 'HR', name: 'Хорватия', nameEn: 'Croatia' },
{ code: 'TD', name: 'Чад', nameEn: 'Chad' },
{ code: 'ME', name: 'Черногория', nameEn: 'Montenegro' },
{ code: 'CZ', name: 'Чехия', nameEn: 'Czechia' },
{ code: 'CL', name: 'Чили', nameEn: 'Chile' },
{ code: 'CH', name: 'Швейцария', nameEn: 'Switzerland' },
{ code: 'SE', name: 'Швеция', nameEn: 'Sweden' },
{ code: 'LK', name: 'Шри-Ланка', nameEn: 'Sri Lanka' },
{ code: 'EC', name: 'Эквадор', nameEn: 'Ecuador' },
{ code: 'GQ', name: 'Экваториальная Гвинея', nameEn: 'Equatorial Guinea' },
{ code: 'ER', name: 'Эритрея', nameEn: 'Eritrea' },
{ code: 'EE', name: 'Эстония', nameEn: 'Estonia' },
{ code: 'ET', name: 'Эфиопия', nameEn: 'Ethiopia' },
{ code: 'ZA', name: 'ЮАР', nameEn: 'South Africa' },
{ code: 'JM', name: 'Ямайка', nameEn: 'Jamaica' },
{ code: 'JP', name: 'Япония', nameEn: 'Japan' },
]
export const COUNTRY_BY_CODE: Record<string, CountryRef> = Object.fromEntries(
COUNTRIES.map((c) => [c.code, c]),
)
export const COUNTRY_BY_NAME_RU: Record<string, CountryRef> = Object.fromEntries(
COUNTRIES.map((c) => [c.name.toLowerCase(), c]),
)
+3
View File
@@ -0,0 +1,3 @@
export * from './countries.js'
export * from './cities.js'
export * from './loc-codes.js'
+80
View File
@@ -0,0 +1,80 @@
import { CITIES, type CityRef } from './cities.js'
/**
* Канонические коды {loc} для hostname (из naming seed / docs).
* Ключ — русское имя города (lower case).
*/
export const CANONICAL_LOC_CODES: Record<string, string> = {
москва: 'msk',
франкфурт: 'fra',
амстердам: 'ams',
хельсинки: 'hel',
париж: 'par',
'санкт-петербург': 'led',
берлин: 'ber',
мюнхен: 'muc',
лондон: 'lon',
'нью-йорк': 'nyc',
токио: 'tyo',
сингапур: 'sin',
дубай: 'dxb',
варшава: 'waw',
прага: 'prg',
вена: 'vie',
рига: 'rix',
таллин: 'tll',
минск: 'msq',
киев: 'iev',
стокгольм: 'arn',
осло: 'osl',
копенгаген: 'cph',
цюрих: 'zrh',
брюссель: 'bru',
мадрид: 'mad',
барселона: 'bcn',
милан: 'mxp',
рим: 'rom',
}
/** Детерминированный уникальный loc-код для города (hostname {loc}). */
export function locCodeForCity(
city: Pick<CityRef, 'name' | 'nameEn' | 'countryCode'>,
usedCodes: ReadonlySet<string>,
): string {
const known = CANONICAL_LOC_CODES[city.name.trim().toLowerCase()]
if (known && !usedCodes.has(known)) return known
if (known) {
// known занят другим — fallback ниже
}
const base = city.nameEn.toLowerCase().replace(/[^a-z]/g, '')
if (!base) {
return `${city.countryCode.toLowerCase()}${usedCodes.size}`
}
for (let len = 3; len <= Math.max(3, base.length); len++) {
const candidate = base.slice(0, len)
if (!usedCodes.has(candidate)) return candidate
}
let i = 2
while (usedCodes.has(`${base.slice(0, 2)}${i}`)) i += 1
return `${base.slice(0, 2)}${i}`
}
/** Все города каталога с выделенными loc-кодами (без коллизий). */
export function catalogCitiesWithLocCodes(): Array<CityRef & { locCode: string }> {
const used = new Set<string>()
// Сначала закрепить канонические, чтобы generate не занял msk раньше Москвы
const ordered = [...CITIES].sort((a, b) => {
const ak = CANONICAL_LOC_CODES[a.name.toLowerCase()] ? 0 : 1
const bk = CANONICAL_LOC_CODES[b.name.toLowerCase()] ? 0 : 1
return ak - bk
})
return ordered.map((city) => {
const locCode = locCodeForCity(city, used)
used.add(locCode)
return { ...city, locCode }
})
}
+1
View File
@@ -1,6 +1,7 @@
export * from "./app-switcher.js";
export * from "./settings.js";
export * from "./fleet.js";
export * from "./geo/index.js";
export class ValidationError extends Error {
constructor(message: string) {