fix(i18n): Turkish locale-aware search and sorting (#3115)

* feat(i18n): add Turkish locale-aware text helpers (search/sort)

* test(i18n): cover null/whitespace edges + document compareTr/normalize contract

* fix(i18n): route dashboard search through Turkish-safe matchesSearch

* fix(i18n): sort user-visible lists with Turkish collation (compareTr)

* fix(i18n): keep providerId tiebreaker as ASCII sort (technical id)

* docs(i18n): document intentional lang=en in global-error boundary

* chore(lint): guard against locale-unsafe toLowerCase().includes search

* fix(i18n): migrate missed provider-name search + harden lint disable placement

* fix(i18n): downgrade no-restricted-syntax to warn (incremental adoption)

The rule errored on ~19 pre-existing toLowerCase().includes() call-sites in
src/app accumulated since this PR's base. Keep it as a warning so the guard-rail
guides future code without breaking the 0-errors lint gate (project policy:
0 errors, warnings tolerated).

---------

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
Oğuzhan Sert
2026-06-04 00:58:01 +03:00
committed by GitHub
parent 80c9ca7096
commit 8dc93ade6d
32 changed files with 258 additions and 66 deletions

View File

@@ -6,6 +6,7 @@ import path from "path";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import { ensureCliConfigWriteAllowed, getCliConfigPaths } from "@/shared/services/cliRuntime";
import { resolveDataDir } from "@/lib/dataPaths";
import { compareTr } from "@/shared/utils/turkishText";
import { codexProfileIdSchema, codexProfileNameSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
@@ -88,7 +89,7 @@ export async function GET(request: Request) {
}
// Sort by name
profiles.sort((a, b) => a.name.localeCompare(b.name));
profiles.sort((a, b) => compareTr(a.name, b.name));
return NextResponse.json({ profiles });
} catch (error) {
console.log("Error listing codex profiles:", error.message);

View File

@@ -134,6 +134,7 @@ async function saveContinueConfig({ baseUrl, apiKey, model }) {
normalizeApiBase(m.apiBase).includes("omniroute") ||
normalizeApiBase(m.apiBase).includes(`localhost:${apiPort}`) ||
normalizeApiBase(m.apiBase).includes(`127.0.0.1:${apiPort}`) ||
// eslint-disable-next-line no-restricted-syntax -- teknik string kontrolü, kullanıcı metni araması değil
String(m.apiKey || "")
.toLowerCase()
.includes("sk_omniroute"))

View File

@@ -14,6 +14,7 @@ import { NextRequest, NextResponse } from "next/server";
import { readFileSync, existsSync } from "fs";
import { getAppLogFilePath } from "@/lib/logEnv";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { matchesSearch } from "@/shared/utils/turkishText";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
const LEVEL_ORDER: Record<string, number> = {
@@ -115,7 +116,7 @@ export async function GET(req: NextRequest) {
// Filter by component
if (componentFilter) {
const comp = entry.component || entry.module || "";
if (!comp.toLowerCase().includes(componentFilter.toLowerCase())) continue;
if (!matchesSearch(comp, componentFilter)) continue;
}
// Normalize timestamp field

View File

@@ -30,7 +30,7 @@ export async function GET(request: NextRequest) {
value: `${m.provider}/${m.model}`,
label: `${m.provider}/${m.model} - ${m.name}`,
}))
.sort((a, b) => a.value.localeCompare(b.value));
.sort((a, b) => a.value.localeCompare(b.value)); // teknik sıralama: ASCII kasıtlı
// Add OpenRouter account models that explicitly support embeddings.
try {
@@ -85,7 +85,7 @@ export async function GET(request: NextRequest) {
});
}
options.sort((a, b) => a.value.localeCompare(b.value));
options.sort((a, b) => a.value.localeCompare(b.value)); // teknik sıralama: ASCII kasıtlı
return NextResponse.json({ models: options });
} catch (error) {

View File

@@ -3,6 +3,7 @@ import { skillRegistry } from "@/lib/skills/registry";
import { parsePaginationParams, buildPaginatedResponse } from "@/shared/types/pagination";
import { getSkillsProviderSetting } from "@/lib/skills/providerSettings";
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { matchesSearch } from "@/shared/utils/turkishText";
const POPULAR_BY_PROVIDER = {
skillsmp: ["web-search", "file-reader", "sql-assistant", "devops-helper", "docs-assistant"],
@@ -18,7 +19,7 @@ export async function GET(request?: Request) {
const provider = await getSkillsProviderSetting();
const url = request?.url || "http://localhost/api/skills";
const parsedUrl = new URL(url);
const query = parsedUrl.searchParams.get("q")?.trim().toLowerCase() || "";
const query = parsedUrl.searchParams.get("q")?.trim() || "";
const modeFilter = parsedUrl.searchParams.get("mode");
const sourceFilter = parsedUrl.searchParams.get("source");
@@ -26,11 +27,11 @@ export async function GET(request?: Request) {
if (query) {
allSkills = allSkills.filter((skill) => {
const tags = Array.isArray(skill.tags) ? skill.tags.join(" ").toLowerCase() : "";
const tagsText = Array.isArray(skill.tags) ? skill.tags.join(" ") : "";
return (
skill.name.toLowerCase().includes(query) ||
skill.description.toLowerCase().includes(query) ||
tags.includes(query)
matchesSearch(skill.name, query) ||
matchesSearch(skill.description, query) ||
matchesSearch(tagsText, query)
);
});
}

View File

@@ -462,6 +462,7 @@ export async function getUnifiedModelsResponse(
? synced.attachment
: syncedInputModalities.length > 0 || syncedOutputModalities.length > 0
? [...syncedInputModalities, ...syncedOutputModalities].some((entry) =>
// eslint-disable-next-line no-restricted-syntax -- teknik string kontrolü, kullanıcı metni araması değil
entry.toLowerCase().includes("image")
)
: undefined;