feat: default locale Russian, geo determines language for other countries

- localization-svc: defaultLocale ru, resolveLocale only by geo
- web-svc: DEFAULT_LOCALE ru, layout lang=ru, embeddedTranslations fallback ru
- countryToLocale: default ru when no country or unknown country

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
home
2026-02-23 15:10:38 +03:00
parent 8fc82a3b90
commit cd6b7857ba
606 changed files with 26148 additions and 14297 deletions

View File

@@ -0,0 +1,173 @@
/**
* Клиент для Localization Service.
* Получает locale на основе геопозиции (через geo-device-service) и переводы.
*/
import { fetchContextWithGeolocation } from './geoDevice';
import { localeFromCountryCode } from './localization/countryToLocale';
const LOCALE_API = '/api/locale';
const TRANSLATIONS_API = '/api/translations';
export interface LocalizationContext {
locale: string;
language: string;
region: string | null;
countryCode: string | null;
timezone: string | null;
source: 'geo' | 'accept-language' | 'client' | 'fallback';
}
const collectClientData = () => {
if (typeof window === 'undefined') return {};
const nav = navigator;
return {
client: {
language: nav.language,
languages: nav.languages ? [...nav.languages] : undefined,
},
};
};
/**
* Получить locale (серверный вызов — без client data).
*/
export async function fetchLocale(): Promise<LocalizationContext> {
const res = await fetch(LOCALE_API);
if (!res.ok) throw new Error('Failed to fetch locale');
return res.json();
}
/**
* Получить locale с данными клиента (браузер language).
*/
export async function fetchLocaleWithClient(): Promise<LocalizationContext> {
const clientData = collectClientData();
const res = await fetch(LOCALE_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(clientData),
});
if (!res.ok) throw new Error('Failed to fetch locale');
return res.json();
}
/** IP-провайдеры возвращают country_code — fallback когда geo-device недоступен */
async function fetchCountryFromIp(): Promise<string | null> {
const providers: Array<{
url: string;
getCountry: (d: Record<string, unknown>) => string | null;
}> = [
{
url: 'https://get.geojs.io/v1/ip/geo.json',
getCountry: (d) => {
const cc = d.country_code ?? d.country;
return typeof cc === 'string' ? cc.toUpperCase() : null;
},
},
{
url: 'https://ipwhois.app/json/',
getCountry: (d) => {
const cc = d.country_code ?? d.country;
return typeof cc === 'string' ? cc.toUpperCase() : null;
},
},
];
for (const p of providers) {
try {
const res = await fetch(p.url);
const d = (await res.json()) as Record<string, unknown>;
const country = p.getCountry(d);
if (country) return country;
} catch {
/* следующий провайдер */
}
}
return null;
}
const DEFAULT_LOCALE = 'ru';
/**
* Получить locale с приоритетом geo-context.
* По умолчанию — русский. Только если геопозиция из другой страны — используем её язык.
*/
export async function fetchLocaleWithGeoFirst(): Promise<LocalizationContext> {
try {
const ctx = await fetchContextWithGeolocation();
const cc = ctx.geo?.countryCode;
if (cc) {
const locale = localeFromCountryCode(cc);
return {
locale,
language: locale,
region: ctx.geo?.region ?? null,
countryCode: cc,
timezone: ctx.geo?.timezone ?? null,
source: 'geo',
};
}
} catch {
/* geo-context недоступен */
}
try {
const countryCode = await fetchCountryFromIp();
if (countryCode) {
const locale = localeFromCountryCode(countryCode);
return {
locale,
language: locale,
region: null,
countryCode,
timezone: null,
source: 'geo',
};
}
} catch {
/* IP fallback недоступен */
}
return {
locale: DEFAULT_LOCALE,
language: DEFAULT_LOCALE,
region: null,
countryCode: null,
timezone: null,
source: 'fallback',
};
}
import {
getEmbeddedTranslations,
translationLocaleFor,
} from './localization/embeddedTranslations';
/**
* Получить переводы для locale.
* При недоступности API использует встроенные переводы.
*/
export async function fetchTranslations(
locale: string,
): Promise<Record<string, string>> {
const fetchLocale = translationLocaleFor(locale);
try {
const res = await fetch(
`${TRANSLATIONS_API}/${encodeURIComponent(fetchLocale)}`,
);
if (res.ok) return res.json();
} catch {
/* API недоступен */
}
return getEmbeddedTranslations(locale);
}
/**
* Получить locale и переводы одним вызовом.
*/
export async function fetchLocalization(): Promise<{
locale: LocalizationContext;
translations: Record<string, string>;
}> {
const locale = await fetchLocaleWithClient();
const translations = await fetchTranslations(locale.locale);
return { locale, translations };
}