fix(i18n): cover settings namespace labels
This commit is contained in:
@@ -975,6 +975,8 @@ export const enCatalog = {
|
|||||||
"languageDetail.namespaces.status": "Statuses",
|
"languageDetail.namespaces.status": "Statuses",
|
||||||
"languageDetail.namespaces.validation": "Validation",
|
"languageDetail.namespaces.validation": "Validation",
|
||||||
"languageDetail.namespaces.api": "API",
|
"languageDetail.namespaces.api": "API",
|
||||||
|
"languageDetail.namespaces.analytics": "Analytics",
|
||||||
|
"languageDetail.namespaces.business": "Business",
|
||||||
"languageDetail.dialog.cancel": "Cancel",
|
"languageDetail.dialog.cancel": "Cancel",
|
||||||
"languageDetail.dialog.activate.title": "Activate language",
|
"languageDetail.dialog.activate.title": "Activate language",
|
||||||
"languageDetail.dialog.activate.description": "{language} will be shown in active language selectors and content forms.",
|
"languageDetail.dialog.activate.description": "{language} will be shown in active language selectors and content forms.",
|
||||||
|
|||||||
@@ -975,6 +975,8 @@ export const trCatalog = {
|
|||||||
"languageDetail.namespaces.status": "Durumlar",
|
"languageDetail.namespaces.status": "Durumlar",
|
||||||
"languageDetail.namespaces.validation": "Doğrulama",
|
"languageDetail.namespaces.validation": "Doğrulama",
|
||||||
"languageDetail.namespaces.api": "API",
|
"languageDetail.namespaces.api": "API",
|
||||||
|
"languageDetail.namespaces.analytics": "Analizler",
|
||||||
|
"languageDetail.namespaces.business": "İş",
|
||||||
"languageDetail.dialog.cancel": "Vazgeç",
|
"languageDetail.dialog.cancel": "Vazgeç",
|
||||||
"languageDetail.dialog.activate.title": "Dili aktifleştir",
|
"languageDetail.dialog.activate.title": "Dili aktifleştir",
|
||||||
"languageDetail.dialog.activate.description": "{language} aktif dil seçimlerinde ve içerik formlarında gösterilecek.",
|
"languageDetail.dialog.activate.description": "{language} aktif dil seçimlerinde ve içerik formlarında gösterilecek.",
|
||||||
|
|||||||
@@ -2,6 +2,44 @@ import fs from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const i18nNamespaces = [
|
||||||
|
"common",
|
||||||
|
"auth",
|
||||||
|
"navigation",
|
||||||
|
"dashboard",
|
||||||
|
"clients",
|
||||||
|
"projects",
|
||||||
|
"tasks",
|
||||||
|
"calendar",
|
||||||
|
"finance",
|
||||||
|
"journal",
|
||||||
|
"chat",
|
||||||
|
"settings",
|
||||||
|
"portal",
|
||||||
|
"status",
|
||||||
|
"validation",
|
||||||
|
"api",
|
||||||
|
"analytics",
|
||||||
|
"business",
|
||||||
|
];
|
||||||
|
const requiredDynamicKeys = [
|
||||||
|
...i18nNamespaces.map((namespace) => `settings.languageDetail.namespaces.${namespace}`),
|
||||||
|
...["light", "dark", "system"].flatMap((mode) => [
|
||||||
|
`settings.appearance.theme.${mode}.label`,
|
||||||
|
`settings.appearance.theme.${mode}.description`,
|
||||||
|
]),
|
||||||
|
...["all", "draft", "active", "archived"].map((status) => `settings.languages.filters.${status}`),
|
||||||
|
...["draft", "active", "archived", "test"].map((status) => `settings.languages.status.${status}`),
|
||||||
|
...["ltr", "rtl"].map((direction) => `settings.languageNew.direction.${direction}`),
|
||||||
|
...["activate", "default", "archive"].flatMap((action) => [
|
||||||
|
`settings.languageDetail.messages.${action}`,
|
||||||
|
`settings.languageDetail.dialog.${action}.title`,
|
||||||
|
`settings.languageDetail.dialog.${action}.description`,
|
||||||
|
`settings.languageDetail.dialog.${action}.confirm`,
|
||||||
|
]),
|
||||||
|
...["defaultSettings", "fallbacks", "userPreferences", "clients", "portalInvitations", "contentTranslations"]
|
||||||
|
.map((key) => `settings.languageDetail.usage.${key}`),
|
||||||
|
];
|
||||||
const localeRoots = {
|
const localeRoots = {
|
||||||
tr: path.join(process.cwd(), "locales", "tr"),
|
tr: path.join(process.cwd(), "locales", "tr"),
|
||||||
en: path.join(process.cwd(), "locales", "en"),
|
en: path.join(process.cwd(), "locales", "en"),
|
||||||
@@ -30,6 +68,8 @@ export function runReleaseGate() {
|
|||||||
|
|
||||||
const authLanguageSelectors = scanAuthLanguageSelectors();
|
const authLanguageSelectors = scanAuthLanguageSelectors();
|
||||||
const missingSettingsProviders = scanSettingsNamespaceProviders();
|
const missingSettingsProviders = scanSettingsNamespaceProviders();
|
||||||
|
const missingDynamicKeys = scanRequiredDynamicKeys(tr, en);
|
||||||
|
const missingUsedLiteralKeys = scanUsedLiteralTranslationKeys(tr, en);
|
||||||
const hardCodedSamples = scanHardCodedUserText();
|
const hardCodedSamples = scanHardCodedUserText();
|
||||||
const failures = {
|
const failures = {
|
||||||
missingInEn,
|
missingInEn,
|
||||||
@@ -37,6 +77,8 @@ export function runReleaseGate() {
|
|||||||
interpolationMismatches,
|
interpolationMismatches,
|
||||||
authLanguageSelectors,
|
authLanguageSelectors,
|
||||||
missingSettingsProviders,
|
missingSettingsProviders,
|
||||||
|
missingDynamicKeys,
|
||||||
|
missingUsedLiteralKeys,
|
||||||
};
|
};
|
||||||
const ok = Object.values(failures).every((rows) => rows.length === 0);
|
const ok = Object.values(failures).every((rows) => rows.length === 0);
|
||||||
const summary = {
|
const summary = {
|
||||||
@@ -50,6 +92,8 @@ export function runReleaseGate() {
|
|||||||
},
|
},
|
||||||
authLanguageSelectors,
|
authLanguageSelectors,
|
||||||
missingSettingsProviders,
|
missingSettingsProviders,
|
||||||
|
missingDynamicKeys,
|
||||||
|
missingUsedLiteralKeys: missingUsedLiteralKeys.slice(0, 50),
|
||||||
hardCodedSamples,
|
hardCodedSamples,
|
||||||
notes: [
|
notes: [
|
||||||
"hardCodedSamples bilgilendirme amaçlıdır; false-positive üretmemesi için gate'i fail ettirmez.",
|
"hardCodedSamples bilgilendirme amaçlıdır; false-positive üretmemesi için gate'i fail ettirmez.",
|
||||||
@@ -62,6 +106,35 @@ export function runReleaseGate() {
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scanRequiredDynamicKeys(tr, en) {
|
||||||
|
return requiredDynamicKeys
|
||||||
|
.filter((key) => !tr.has(key) || !en.has(key))
|
||||||
|
.map((key) => ({ key, tr: tr.has(key), en: en.has(key) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanUsedLiteralTranslationKeys(tr, en) {
|
||||||
|
const roots = ["app", "components"].map((root) => path.join(process.cwd(), root));
|
||||||
|
const used = new Map();
|
||||||
|
const pattern = /\bt\(\s*["']([a-zA-Z][\w]*\.[^"'`{}]+)["']/g;
|
||||||
|
|
||||||
|
for (const file of roots.flatMap((root) => (fs.existsSync(root) ? listFiles(root) : []))) {
|
||||||
|
if (!/\.(tsx?|jsx?)$/.test(file)) continue;
|
||||||
|
const source = fs.readFileSync(file, "utf8");
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(source))) {
|
||||||
|
const key = match[1];
|
||||||
|
const files = used.get(key) ?? new Set();
|
||||||
|
files.add(path.relative(process.cwd(), file));
|
||||||
|
used.set(key, files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...used.entries()]
|
||||||
|
.filter(([key]) => !tr.has(key) || !en.has(key))
|
||||||
|
.map(([key, files]) => ({ key, tr: tr.has(key), en: en.has(key), files: [...files].slice(0, 5) }))
|
||||||
|
.sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
}
|
||||||
|
|
||||||
function scanSettingsNamespaceProviders() {
|
function scanSettingsNamespaceProviders() {
|
||||||
const requiredFiles = [
|
const requiredFiles = [
|
||||||
{ file: path.join(process.cwd(), "app", "(dashboard)", "layout.tsx"), label: "admin dashboard shell" },
|
{ file: path.join(process.cwd(), "app", "(dashboard)", "layout.tsx"), label: "admin dashboard shell" },
|
||||||
@@ -83,18 +156,43 @@ function scanSettingsNamespaceProviders() {
|
|||||||
function collectLocaleKeys(root) {
|
function collectLocaleKeys(root) {
|
||||||
const files = listFiles(root).filter((file) => file.endsWith(".ts"));
|
const files = listFiles(root).filter((file) => file.endsWith(".ts"));
|
||||||
const entries = new Map();
|
const entries = new Map();
|
||||||
const keyPattern = /"([^"]+)"\s*:\s*"((?:\\"|[^"])*)"/g;
|
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const source = fs.readFileSync(file, "utf8");
|
const source = fs.readFileSync(file, "utf8");
|
||||||
let match;
|
if (path.basename(file) === "common.ts") {
|
||||||
while ((match = keyPattern.exec(source))) {
|
collectObjectKeys(source, "common", entries);
|
||||||
entries.set(match[1], match[2]);
|
continue;
|
||||||
|
}
|
||||||
|
if (path.basename(file) !== "index.ts") continue;
|
||||||
|
|
||||||
|
let namespace = null;
|
||||||
|
for (const line of source.split("\n")) {
|
||||||
|
const namespaceMatch = line.match(/^ ([a-zA-Z_][\w]*): \{/);
|
||||||
|
if (namespaceMatch) {
|
||||||
|
namespace = namespaceMatch[1];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (namespace && line.startsWith(" },")) {
|
||||||
|
namespace = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const keyMatch = line.match(/^\s+"([^"]+)"\s*:\s*"((?:\\"|[^"])*)"/);
|
||||||
|
if (namespace && keyMatch) {
|
||||||
|
entries.set(`${namespace}.${keyMatch[1]}`, keyMatch[2]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectObjectKeys(source, namespace, entries) {
|
||||||
|
const keyPattern = /"([^"]+)"\s*:\s*"((?:\\"|[^"])*)"/g;
|
||||||
|
let match;
|
||||||
|
while ((match = keyPattern.exec(source))) {
|
||||||
|
entries.set(`${namespace}.${match[1]}`, match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function interpolationVariables(value) {
|
function interpolationVariables(value) {
|
||||||
return [...new Set([...value.matchAll(/\{([a-zA-Z][\w.-]*)(?:[,}])/g)].map((match) => match[1]))].sort();
|
return [...new Set([...value.matchAll(/\{([a-zA-Z][\w.-]*)(?:[,}])/g)].map((match) => match[1]))].sort();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user