refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LocaleCode} from '@fluxer/constants/src/Locales';
import {
getLocaleByCode,
getLocaleDisplayName,
getLocaleFlagCode,
getLocaleLanguageCode,
} from '@fluxer/locale/src/catalog/LocaleCatalog';
import {resolveLocaleFromAcceptLanguageHeader} from '@fluxer/locale/src/resolution/AcceptLanguageNegotiation';
export interface LocaleMetadata {
code: LocaleCode;
languageCode: string;
name: string;
flagCode: string;
}
export function getLocaleMetadata(locale: LocaleCode): LocaleMetadata {
return {
code: locale,
languageCode: getLocaleLanguageCode(locale),
name: getLocaleDisplayName(locale),
flagCode: getLocaleFlagCode(locale),
};
}
export function getLocaleName(locale: LocaleCode): string {
return getLocaleDisplayName(locale);
}
export function getFlagCode(locale: LocaleCode): string {
return getLocaleFlagCode(locale);
}
export function getLocaleFromCode(code: string): LocaleCode | null {
return getLocaleByCode(code);
}
export function getLocaleCode(locale: LocaleCode): string {
return getLocaleLanguageCode(locale);
}
export function parseAcceptLanguage(acceptLanguageHeader: string | null | undefined): LocaleCode {
return resolveLocaleFromAcceptLanguageHeader(acceptLanguageHeader);
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {AllLocales, type LocaleCode, Locales} from '@fluxer/constants/src/Locales';
interface LocaleMetadata {
displayName: string;
flagCode: string;
aliases?: ReadonlyArray<string>;
}
export const DEFAULT_LOCALE: LocaleCode = Locales.EN_US;
export const SUPPORTED_LOCALES: ReadonlyArray<LocaleCode> = AllLocales;
const SUPPORTED_LOCALE_SET: ReadonlySet<LocaleCode> = new Set<LocaleCode>(SUPPORTED_LOCALES);
const LANGUAGE_FALLBACK_BY_LANGUAGE_CODE: Record<string, LocaleCode> = {
en: Locales.EN_US,
es: Locales.ES_ES,
pt: Locales.PT_BR,
zh: Locales.ZH_CN,
sv: Locales.SV_SE,
};
const LOCALE_METADATA_BY_CODE: Record<LocaleCode, LocaleMetadata> = {
[Locales.AR]: {
displayName: 'العربية',
flagCode: '1f1f8-1f1e6',
},
[Locales.BG]: {
displayName: 'Български',
flagCode: '1f1e7-1f1ec',
},
[Locales.CS]: {
displayName: 'Čeština',
flagCode: '1f1e8-1f1ff',
},
[Locales.DA]: {
displayName: 'Dansk',
flagCode: '1f1e9-1f1f0',
},
[Locales.DE]: {
displayName: 'Deutsch',
flagCode: '1f1e9-1f1ea',
},
[Locales.EL]: {
displayName: 'Ελληνικά',
flagCode: '1f1ec-1f1f7',
},
[Locales.EN_GB]: {
displayName: 'English',
flagCode: '1f1ec-1f1e7',
},
[Locales.EN_US]: {
displayName: 'English (US)',
flagCode: '1f1fa-1f1f8',
aliases: ['en'],
},
[Locales.ES_ES]: {
displayName: 'Español (España)',
flagCode: '1f1ea-1f1f8',
},
[Locales.ES_419]: {
displayName: 'Español (Latinoamérica)',
flagCode: '1f30e',
},
[Locales.FI]: {
displayName: 'Suomi',
flagCode: '1f1eb-1f1ee',
},
[Locales.FR]: {
displayName: 'Français',
flagCode: '1f1eb-1f1f7',
},
[Locales.HE]: {
displayName: 'עברית',
flagCode: '1f1ee-1f1f1',
},
[Locales.HI]: {
displayName: 'हिन्दी',
flagCode: '1f1ee-1f1f3',
},
[Locales.HR]: {
displayName: 'Hrvatski',
flagCode: '1f1ed-1f1f7',
},
[Locales.HU]: {
displayName: 'Magyar',
flagCode: '1f1ed-1f1fa',
},
[Locales.ID]: {
displayName: 'Bahasa Indonesia',
flagCode: '1f1ee-1f1e9',
},
[Locales.IT]: {
displayName: 'Italiano',
flagCode: '1f1ee-1f1f9',
},
[Locales.JA]: {
displayName: '日本語',
flagCode: '1f1ef-1f1f5',
},
[Locales.KO]: {
displayName: '한국어',
flagCode: '1f1f0-1f1f7',
},
[Locales.LT]: {
displayName: 'Lietuvių',
flagCode: '1f1f1-1f1f9',
},
[Locales.NL]: {
displayName: 'Nederlands',
flagCode: '1f1f3-1f1f1',
},
[Locales.NO]: {
displayName: 'Norsk',
flagCode: '1f1f3-1f1f4',
},
[Locales.PL]: {
displayName: 'Polski',
flagCode: '1f1f5-1f1f1',
},
[Locales.PT_BR]: {
displayName: 'Português (Brasil)',
flagCode: '1f1e7-1f1f7',
},
[Locales.RO]: {
displayName: 'Română',
flagCode: '1f1f7-1f1f4',
},
[Locales.RU]: {
displayName: 'Русский',
flagCode: '1f1f7-1f1fa',
},
[Locales.SV_SE]: {
displayName: 'Svenska',
flagCode: '1f1f8-1f1ea',
aliases: ['sv'],
},
[Locales.TH]: {
displayName: 'ไทย',
flagCode: '1f1f9-1f1ed',
},
[Locales.TR]: {
displayName: 'Türkçe',
flagCode: '1f1f9-1f1f7',
},
[Locales.UK]: {
displayName: 'Українська',
flagCode: '1f1fa-1f1e6',
},
[Locales.VI]: {
displayName: 'Tiếng Việt',
flagCode: '1f1fb-1f1f3',
},
[Locales.ZH_CN]: {
displayName: '简体中文',
flagCode: '1f1e8-1f1f3',
},
[Locales.ZH_TW]: {
displayName: '繁體中文',
flagCode: '1f1f9-1f1fc',
},
};
const NORMALIZED_LOCALE_TO_CODE = createNormalizedLocaleLookup();
export function normalizeLocaleCode(code: string): string {
return code.trim().replace(/_/g, '-').toLowerCase();
}
export function isSupportedLocale(locale: LocaleCode): boolean {
return SUPPORTED_LOCALE_SET.has(locale);
}
export function getLocaleDisplayName(locale: LocaleCode): string {
return LOCALE_METADATA_BY_CODE[locale].displayName;
}
export function getLocaleFlagCode(locale: LocaleCode): string {
return LOCALE_METADATA_BY_CODE[locale].flagCode;
}
export function getLocaleByCode(code: string): LocaleCode | null {
const normalizedCode = normalizeLocaleCode(code);
if (!normalizedCode) {
return null;
}
const locale = NORMALIZED_LOCALE_TO_CODE.get(normalizedCode);
return locale ?? null;
}
export function getLocaleLanguageCode(locale: LocaleCode): string {
return locale.split('-')[0];
}
export function getPreferredLocaleForLanguageCode(languageCode: string): LocaleCode | null {
const normalizedLanguageCode = normalizeLocaleCode(languageCode);
if (!normalizedLanguageCode) {
return null;
}
const preferredLocale = LANGUAGE_FALLBACK_BY_LANGUAGE_CODE[normalizedLanguageCode];
if (!preferredLocale) {
return null;
}
if (!isSupportedLocale(preferredLocale)) {
return null;
}
return preferredLocale;
}
export function findLocaleByLanguagePrefix(languageCode: string): LocaleCode | null {
const normalizedLanguageCode = normalizeLocaleCode(languageCode);
if (!normalizedLanguageCode) {
return null;
}
const languagePrefix = `${normalizedLanguageCode}-`;
for (const locale of SUPPORTED_LOCALES) {
if (locale.toLowerCase().startsWith(languagePrefix)) {
return locale;
}
}
return null;
}
function createNormalizedLocaleLookup(): ReadonlyMap<string, LocaleCode> {
const lookup = new Map<string, LocaleCode>();
for (const locale of SUPPORTED_LOCALES) {
lookup.set(normalizeLocaleCode(locale), locale);
}
for (const locale of SUPPORTED_LOCALES) {
const aliases = LOCALE_METADATA_BY_CODE[locale].aliases ?? [];
for (const alias of aliases) {
lookup.set(normalizeLocaleCode(alias), locale);
}
}
return lookup;
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {LocaleCode} from '@fluxer/constants/src/Locales';
import {
DEFAULT_LOCALE,
findLocaleByLanguagePrefix,
getLocaleByCode,
getPreferredLocaleForLanguageCode,
normalizeLocaleCode,
} from '@fluxer/locale/src/catalog/LocaleCatalog';
interface AcceptLanguagePreference {
locale: string;
quality: number;
}
export function resolveLocaleFromAcceptLanguageHeader(acceptLanguageHeader: string | null | undefined): LocaleCode {
if (!acceptLanguageHeader) {
return DEFAULT_LOCALE;
}
try {
const preferences = parseAcceptLanguagePreferences(acceptLanguageHeader);
const exactLocaleMatch = findExactLocaleMatch(preferences);
if (exactLocaleMatch) {
return exactLocaleMatch;
}
const languageFallbackMatch = findLanguageFallbackMatch(preferences);
if (languageFallbackMatch) {
return languageFallbackMatch;
}
return DEFAULT_LOCALE;
} catch {
return DEFAULT_LOCALE;
}
}
function parseAcceptLanguagePreferences(acceptLanguageHeader: string): Array<AcceptLanguagePreference> {
return acceptLanguageHeader
.split(',')
.map((languageToken) => parseLanguagePreference(languageToken))
.sort((a, b) => b.quality - a.quality);
}
function parseLanguagePreference(languageToken: string): AcceptLanguagePreference {
const parts = languageToken.trim().split(';');
const locale = parts[0].trim();
const quality = parseQualityValue(parts[1]);
return {
locale,
quality,
};
}
function parseQualityValue(rawQuality: string | undefined): number {
const qualityMatch = rawQuality?.match(/q=([\d.]+)/);
if (!qualityMatch) {
return 1.0;
}
const parsedQuality = Number.parseFloat(qualityMatch[1]);
if (!Number.isFinite(parsedQuality)) {
return 1.0;
}
return parsedQuality;
}
function findExactLocaleMatch(preferences: ReadonlyArray<AcceptLanguagePreference>): LocaleCode | null {
for (const {locale} of preferences) {
const exactMatch = getLocaleByCode(locale);
if (exactMatch) {
return exactMatch;
}
}
return null;
}
function findLanguageFallbackMatch(preferences: ReadonlyArray<AcceptLanguagePreference>): LocaleCode | null {
for (const {locale} of preferences) {
const languageCode = getLanguageCode(locale);
const preferredFallbackLocale = getPreferredLocaleForLanguageCode(languageCode);
if (preferredFallbackLocale) {
return preferredFallbackLocale;
}
const prefixMatchLocale = findLocaleByLanguagePrefix(languageCode);
if (prefixMatchLocale) {
return prefixMatchLocale;
}
}
return null;
}
function getLanguageCode(locale: string): string {
const normalizedLocale = normalizeLocaleCode(locale);
const [languageCode] = normalizedLocale.split('-');
return languageCode ?? '';
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {AllLocales, type LocaleCode, Locales} from '@fluxer/constants/src/Locales';
import {
getFlagCode,
getLocaleCode,
getLocaleFromCode,
getLocaleMetadata,
getLocaleName,
parseAcceptLanguage,
} from '@fluxer/locale/src/LocaleService';
import {describe, expect, it} from 'vitest';
interface LocaleLookupCase {
input: string;
expected: LocaleCode | null;
}
interface AcceptLanguageCase {
header: string | null | undefined;
expected: LocaleCode;
}
const localeNameCases: Array<{locale: LocaleCode; expected: string}> = [
{locale: Locales.EN_US, expected: 'English (US)'},
{locale: Locales.EN_GB, expected: 'English'},
{locale: Locales.DE, expected: 'Deutsch'},
{locale: Locales.JA, expected: '日本語'},
{locale: Locales.ZH_CN, expected: '简体中文'},
{locale: Locales.ZH_TW, expected: '繁體中文'},
{locale: Locales.ES_419, expected: 'Español (Latinoamérica)'},
{locale: Locales.PT_BR, expected: 'Português (Brasil)'},
];
const flagCodeCases: Array<{locale: LocaleCode; expected: string}> = [
{locale: Locales.EN_US, expected: '1f1fa-1f1f8'},
{locale: Locales.EN_GB, expected: '1f1ec-1f1e7'},
{locale: Locales.DE, expected: '1f1e9-1f1ea'},
{locale: Locales.JA, expected: '1f1ef-1f1f5'},
{locale: Locales.ZH_CN, expected: '1f1e8-1f1f3'},
{locale: Locales.ES_419, expected: '1f30e'},
{locale: Locales.PT_BR, expected: '1f1e7-1f1f7'},
];
const localeLookupCases: Array<LocaleLookupCase> = [
{input: 'en', expected: Locales.EN_US},
{input: 'en-US', expected: Locales.EN_US},
{input: 'en-us', expected: Locales.EN_US},
{input: 'en_GB', expected: Locales.EN_GB},
{input: 'en-gb', expected: Locales.EN_GB},
{input: 'de', expected: Locales.DE},
{input: 'ja', expected: Locales.JA},
{input: 'zh-CN', expected: Locales.ZH_CN},
{input: 'zh-cn', expected: Locales.ZH_CN},
{input: 'zh-TW', expected: Locales.ZH_TW},
{input: 'zh-tw', expected: Locales.ZH_TW},
{input: 'es-419', expected: Locales.ES_419},
{input: 'pt-br', expected: Locales.PT_BR},
{input: 'sv', expected: Locales.SV_SE},
{input: 'sv-se', expected: Locales.SV_SE},
{input: 'xx', expected: null},
{input: '', expected: null},
{input: 'en-AU', expected: null},
];
const localeCodeCases: Array<{locale: LocaleCode; expected: string}> = [
{locale: Locales.EN_US, expected: 'en'},
{locale: Locales.EN_GB, expected: 'en'},
{locale: Locales.DE, expected: 'de'},
{locale: Locales.ZH_CN, expected: 'zh'},
{locale: Locales.ZH_TW, expected: 'zh'},
{locale: Locales.ES_ES, expected: 'es'},
{locale: Locales.ES_419, expected: 'es'},
{locale: Locales.PT_BR, expected: 'pt'},
{locale: Locales.SV_SE, expected: 'sv'},
{locale: Locales.JA, expected: 'ja'},
];
const acceptLanguageCases: Array<AcceptLanguageCase> = [
{header: null, expected: Locales.EN_US},
{header: undefined, expected: Locales.EN_US},
{header: '', expected: Locales.EN_US},
{header: 'de', expected: Locales.DE},
{header: 'en-GB', expected: Locales.EN_GB},
{header: 'en-gb', expected: Locales.EN_GB},
{header: 'de;q=0.9, fr;q=1.0', expected: Locales.FR},
{header: 'en-US,en;q=0.9,de;q=0.8', expected: Locales.EN_US},
{header: 'en-AU', expected: Locales.EN_US},
{header: 'es', expected: Locales.ES_ES},
{header: 'pt', expected: Locales.PT_BR},
{header: 'zh', expected: Locales.ZH_CN},
{header: 'sv', expected: Locales.SV_SE},
{header: 'de;q=0.5, ja;q=0.9, fr;q=0.7', expected: Locales.JA},
{header: 'de, fr;q=0.5', expected: Locales.DE},
{header: 'xx-YY', expected: Locales.EN_US},
{header: 'fr-CA', expected: Locales.EN_US},
{header: ' de , fr ', expected: Locales.DE},
{header: 'zh-TW', expected: Locales.ZH_TW},
{header: 'zh-CN', expected: Locales.ZH_CN},
{header: 'es-419', expected: Locales.ES_419},
{header: 'pt-BR', expected: Locales.PT_BR},
{header: 'sv-SE', expected: Locales.SV_SE},
{header: 'xx-YY, zz-AA, qq-BB', expected: Locales.EN_US},
{header: 'zh-TW, zh;q=0.9', expected: Locales.ZH_TW},
{header: 'en-US,en;q=0.9,ja;q=0.8,de;q=0.7,fr;q=0.6', expected: Locales.EN_US},
{header: 'de;q=0.999, fr;q=0.998', expected: Locales.DE},
{header: 'de;q=0, fr;q=1', expected: Locales.FR},
];
describe('LocaleService', () => {
describe('getLocaleMetadata', () => {
it('returns consolidated metadata for a locale', () => {
expect(getLocaleMetadata(Locales.EN_US)).toEqual({
code: Locales.EN_US,
languageCode: 'en',
name: 'English (US)',
flagCode: '1f1fa-1f1f8',
});
});
it('returns metadata for every supported locale', () => {
for (const locale of AllLocales) {
const metadata = getLocaleMetadata(locale);
expect(metadata.code).toBe(locale);
expect(metadata.name.length).toBeGreaterThan(0);
expect(metadata.flagCode.length).toBeGreaterThan(0);
}
});
});
describe('getLocaleName', () => {
for (const {locale, expected} of localeNameCases) {
it(`returns ${expected} for ${locale}`, () => {
expect(getLocaleName(locale)).toBe(expected);
});
}
});
describe('getFlagCode', () => {
for (const {locale, expected} of flagCodeCases) {
it(`returns ${expected} for ${locale}`, () => {
expect(getFlagCode(locale)).toBe(expected);
});
}
});
describe('getLocaleFromCode', () => {
for (const {input, expected} of localeLookupCases) {
it(`resolves ${input || 'empty'} to ${expected ?? 'null'}`, () => {
expect(getLocaleFromCode(input)).toBe(expected);
});
}
it('resolves canonical locale codes for all supported locales', () => {
for (const locale of AllLocales) {
expect(getLocaleFromCode(locale)).toBe(locale);
}
});
});
describe('getLocaleCode', () => {
for (const {locale, expected} of localeCodeCases) {
it(`returns ${expected} for ${locale}`, () => {
expect(getLocaleCode(locale)).toBe(expected);
});
}
});
describe('parseAcceptLanguage', () => {
for (const {header, expected} of acceptLanguageCases) {
it(`selects ${expected} for ${header ?? 'nullish header'}`, () => {
expect(parseAcceptLanguage(header)).toBe(expected);
});
}
});
});