feat(history): track translation and config changes in edit history
Record localized field updates across all translatable entities. Track sort order modifications and specific config properties. Support parsing and translating localized field labels in the UI.
This commit is contained in:
@@ -402,6 +402,9 @@ type EditHistoryEntry = {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
user: { id: number; displayName: string } | null;
|
user: { id: number; displayName: string } | null;
|
||||||
};
|
};
|
||||||
|
type TranslationChangeSource = {
|
||||||
|
translations?: TranslationInput | null;
|
||||||
|
};
|
||||||
type PokemonChangeSource = {
|
type PokemonChangeSource = {
|
||||||
displayId: number;
|
displayId: number;
|
||||||
isEventItem: boolean;
|
isEventItem: boolean;
|
||||||
@@ -416,7 +419,7 @@ type PokemonChangeSource = {
|
|||||||
environment: { name: string };
|
environment: { name: string };
|
||||||
skills: Array<{ name: string; itemDrop?: { name: string } | null }>;
|
skills: Array<{ name: string; itemDrop?: { name: string } | null }>;
|
||||||
favorite_things: Array<{ name: string }>;
|
favorite_things: Array<{ name: string }>;
|
||||||
};
|
} & TranslationChangeSource;
|
||||||
type ItemChangeSource = {
|
type ItemChangeSource = {
|
||||||
name: string;
|
name: string;
|
||||||
isEventItem: boolean;
|
isEventItem: boolean;
|
||||||
@@ -427,19 +430,29 @@ type ItemChangeSource = {
|
|||||||
noRecipe: boolean;
|
noRecipe: boolean;
|
||||||
acquisitionMethods: Array<{ name: string }>;
|
acquisitionMethods: Array<{ name: string }>;
|
||||||
tags: Array<{ name: string }>;
|
tags: Array<{ name: string }>;
|
||||||
};
|
} & TranslationChangeSource;
|
||||||
type HabitatChangeSource = {
|
type HabitatChangeSource = {
|
||||||
name: string;
|
name: string;
|
||||||
isEventItem: boolean;
|
isEventItem: boolean;
|
||||||
image: EntityImageValue | null;
|
image: EntityImageValue | null;
|
||||||
recipe: Array<{ name: string; quantity: number }>;
|
recipe: Array<{ name: string; quantity: number }>;
|
||||||
pokemon: Array<{ name: string; time_of_day: string; weather: string; rarity: number; map: { name: string } }>;
|
pokemon: Array<{ name: string; time_of_day: string; weather: string; rarity: number; map: { name: string } }>;
|
||||||
};
|
} & TranslationChangeSource;
|
||||||
type RecipeChangeSource = {
|
type RecipeChangeSource = {
|
||||||
item: { name: string };
|
item: { name: string };
|
||||||
acquisition_methods: Array<{ name: string }>;
|
acquisition_methods: Array<{ name: string }>;
|
||||||
materials: Array<{ name: string; quantity: number }>;
|
materials: Array<{ name: string; quantity: number }>;
|
||||||
};
|
};
|
||||||
|
type DailyChecklistChangeSource = {
|
||||||
|
title: string;
|
||||||
|
} & TranslationChangeSource;
|
||||||
|
type ConfigChangeSource = {
|
||||||
|
name: string;
|
||||||
|
hasItemDrop?: boolean;
|
||||||
|
isDefault?: boolean;
|
||||||
|
isRateable?: boolean;
|
||||||
|
changeLog?: string;
|
||||||
|
} & TranslationChangeSource;
|
||||||
|
|
||||||
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
||||||
const weathers = ['晴天', '阴天', '雨天'];
|
const weathers = ['晴天', '阴天', '雨天'];
|
||||||
@@ -915,8 +928,8 @@ async function reorderTableRows(
|
|||||||
ids: number[],
|
ids: number[],
|
||||||
userId: number
|
userId: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const existing = await client.query<{ id: number }>(
|
const existing = await client.query<{ id: number; sortOrder: number }>(
|
||||||
`SELECT id FROM ${tableName} WHERE id = ANY($1::integer[])`,
|
`SELECT id, sort_order AS "sortOrder" FROM ${tableName} WHERE id = ANY($1::integer[])`,
|
||||||
[ids]
|
[ids]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -924,16 +937,25 @@ async function reorderTableRows(
|
|||||||
throw validationError('server.validation.recordMissing');
|
throw validationError('server.validation.recordMissing');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortOrders = new Map(existing.rows.map((row) => [row.id, row.sortOrder]));
|
||||||
for (const [index, id] of ids.entries()) {
|
for (const [index, id] of ids.entries()) {
|
||||||
|
const nextSortOrder = (index + 1) * 10;
|
||||||
|
const previousSortOrder = sortOrders.get(id);
|
||||||
|
if (previousSortOrder === nextSortOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
UPDATE ${tableName}
|
UPDATE ${tableName}
|
||||||
SET sort_order = $1, updated_by_user_id = $2, updated_at = now()
|
SET sort_order = $1, updated_by_user_id = $2, updated_at = now()
|
||||||
WHERE id = $3
|
WHERE id = $3
|
||||||
`,
|
`,
|
||||||
[(index + 1) * 10, userId, id]
|
[nextSortOrder, userId, id]
|
||||||
);
|
);
|
||||||
await recordEditLog(client, entityType, id, 'update', userId);
|
const changes: EditChange[] = [];
|
||||||
|
pushChange(changes, 'Sort order', String(previousSortOrder), String(nextSortOrder));
|
||||||
|
await recordEditLog(client, entityType, id, 'update', userId, changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1608,7 +1630,14 @@ async function ensurePokemonTypeCatalog(
|
|||||||
const typeId = csvInteger(row, 'type_id');
|
const typeId = csvInteger(row, 'type_id');
|
||||||
const name = defaultCsvText(row, languages, csvText(row, 'identifier'));
|
const name = defaultCsvText(row, languages, csvText(row, 'identifier'));
|
||||||
const translations = localizedCsvTranslations([{ row, fieldName: 'name' }], languages);
|
const translations = localizedCsvTranslations([{ row, fieldName: 'name' }], languages);
|
||||||
const existing = await client.query<{ name: string }>('SELECT name FROM pokemon_types WHERE id = $1', [typeId]);
|
const existing = await client.query<ConfigChangeSource>(
|
||||||
|
`
|
||||||
|
SELECT pt.name, ${translationsSelect('pokemon-types', 'pt.id')} AS translations
|
||||||
|
FROM pokemon_types pt
|
||||||
|
WHERE pt.id = $1
|
||||||
|
`,
|
||||||
|
[typeId]
|
||||||
|
);
|
||||||
|
|
||||||
if (existing.rowCount === 0) {
|
if (existing.rowCount === 0) {
|
||||||
await client.query(
|
await client.query(
|
||||||
@@ -1625,20 +1654,25 @@ async function ensurePokemonTypeCatalog(
|
|||||||
[typeId, name, typeId * 10, userId]
|
[typeId, name, typeId * 10, userId]
|
||||||
);
|
);
|
||||||
await recordEditLog(client, 'pokemon-types', typeId, 'create', userId);
|
await recordEditLog(client, 'pokemon-types', typeId, 'create', userId);
|
||||||
} else if (existing.rows[0].name !== name) {
|
} else {
|
||||||
await client.query(
|
const changes = configEditChanges(
|
||||||
`
|
{ table: 'pokemon_types', entityType: 'pokemon-types' },
|
||||||
UPDATE pokemon_types
|
existing.rows[0],
|
||||||
SET name = $1,
|
{ name, translations, hasItemDrop: false, isDefault: false, isRateable: false, changeLog: '' }
|
||||||
updated_by_user_id = $2,
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = $3
|
|
||||||
`,
|
|
||||||
[name, userId, typeId]
|
|
||||||
);
|
);
|
||||||
await recordEditLog(client, 'pokemon-types', typeId, 'update', userId, [
|
if (changes.length) {
|
||||||
{ label: 'Name', before: existing.rows[0].name, after: name }
|
await client.query(
|
||||||
]);
|
`
|
||||||
|
UPDATE pokemon_types
|
||||||
|
SET name = $1,
|
||||||
|
updated_by_user_id = $2,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $3
|
||||||
|
`,
|
||||||
|
[name, userId, typeId]
|
||||||
|
);
|
||||||
|
await recordEditLog(client, 'pokemon-types', typeId, 'update', userId, changes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await replaceEntityTranslations(client, 'pokemon-types', typeId, translations, ['name']);
|
await replaceEntityTranslations(client, 'pokemon-types', typeId, translations, ['name']);
|
||||||
@@ -1777,6 +1811,44 @@ function pushChange(changes: EditChange[], label: string, before: string | null
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const translationChangeLabels: Record<TranslationField, string> = {
|
||||||
|
name: 'Name',
|
||||||
|
title: 'Title',
|
||||||
|
details: 'Details',
|
||||||
|
genus: 'Genus'
|
||||||
|
};
|
||||||
|
|
||||||
|
function translationFieldValue(
|
||||||
|
translations: TranslationInput | null | undefined,
|
||||||
|
locale: string,
|
||||||
|
field: TranslationField
|
||||||
|
): string | null {
|
||||||
|
const value = translations?.[locale]?.[field];
|
||||||
|
return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushTranslationChanges(
|
||||||
|
changes: EditChange[],
|
||||||
|
before: TranslationInput | null | undefined,
|
||||||
|
after: TranslationInput,
|
||||||
|
fields: TranslationField[]
|
||||||
|
): void {
|
||||||
|
const locales = [...new Set([...Object.keys(before ?? {}), ...Object.keys(after)])]
|
||||||
|
.filter((locale) => locale !== defaultLocale)
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
for (const locale of locales) {
|
||||||
|
for (const field of fields) {
|
||||||
|
pushChange(
|
||||||
|
changes,
|
||||||
|
`${translationChangeLabels[field]} (${locale})`,
|
||||||
|
translationFieldValue(before, locale, field),
|
||||||
|
translationFieldValue(after, locale, field)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function boolValue(value: boolean): string {
|
function boolValue(value: boolean): string {
|
||||||
return value ? 'Yes' : 'No';
|
return value ? 'Yes' : 'No';
|
||||||
}
|
}
|
||||||
@@ -1917,6 +1989,7 @@ async function pokemonEditChanges(
|
|||||||
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem));
|
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem));
|
||||||
pushChange(changes, 'Genus', before.genus, after.genus);
|
pushChange(changes, 'Genus', before.genus, after.genus);
|
||||||
pushChange(changes, 'Details', before.details, after.details);
|
pushChange(changes, 'Details', before.details, after.details);
|
||||||
|
pushTranslationChanges(changes, before.translations, after.translations, ['name', 'genus', 'details']);
|
||||||
pushChange(changes, 'Height', pokemonHeightValue(before.heightInches), pokemonHeightValue(after.heightInches));
|
pushChange(changes, 'Height', pokemonHeightValue(before.heightInches), pokemonHeightValue(after.heightInches));
|
||||||
pushChange(changes, 'Weight', pokemonWeightValue(before.weightPounds), pokemonWeightValue(after.weightPounds));
|
pushChange(changes, 'Weight', pokemonWeightValue(before.weightPounds), pokemonWeightValue(after.weightPounds));
|
||||||
pushChange(changes, 'Image', pokemonImageLabel(before.image), pokemonImageLabel(after.image));
|
pushChange(changes, 'Image', pokemonImageLabel(before.image), pokemonImageLabel(after.image));
|
||||||
@@ -1942,6 +2015,7 @@ async function itemEditChanges(
|
|||||||
const tagNames = await entityNameMap(client, 'favorite_things', after.tagIds);
|
const tagNames = await entityNameMap(client, 'favorite_things', after.tagIds);
|
||||||
|
|
||||||
pushChange(changes, 'Name', before.name, after.name);
|
pushChange(changes, 'Name', before.name, after.name);
|
||||||
|
pushTranslationChanges(changes, before.translations, after.translations, ['name']);
|
||||||
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem));
|
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem));
|
||||||
pushChange(changes, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath));
|
pushChange(changes, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath));
|
||||||
pushChange(changes, 'Category', before.category.name, categoryNames.get(after.categoryId));
|
pushChange(changes, 'Category', before.category.name, categoryNames.get(after.categoryId));
|
||||||
@@ -1975,6 +2049,7 @@ async function habitatEditChanges(
|
|||||||
.join(' / ');
|
.join(' / ');
|
||||||
|
|
||||||
pushChange(changes, 'Name', before.name, after.name);
|
pushChange(changes, 'Name', before.name, after.name);
|
||||||
|
pushTranslationChanges(changes, before.translations, after.translations, ['name']);
|
||||||
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem));
|
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem));
|
||||||
pushChange(changes, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath));
|
pushChange(changes, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath));
|
||||||
pushChange(changes, 'Recipe', quantityListValue(before.recipe), await quantityPayloadValue(client, after.recipeItems));
|
pushChange(changes, 'Recipe', quantityListValue(before.recipe), await quantityPayloadValue(client, after.recipeItems));
|
||||||
@@ -1999,6 +2074,43 @@ async function recipeEditChanges(
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dailyChecklistEditChanges(before: DailyChecklistChangeSource, after: DailyChecklistPayload): EditChange[] {
|
||||||
|
const changes: EditChange[] = [];
|
||||||
|
pushChange(changes, 'Title', before.title, after.title);
|
||||||
|
pushTranslationChanges(changes, before.translations, after.translations, ['title']);
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configEditChanges(
|
||||||
|
definition: ConfigDefinition,
|
||||||
|
before: ConfigChangeSource,
|
||||||
|
after: {
|
||||||
|
name: string;
|
||||||
|
translations: TranslationInput;
|
||||||
|
hasItemDrop: boolean;
|
||||||
|
isDefault: boolean;
|
||||||
|
isRateable: boolean;
|
||||||
|
changeLog: string;
|
||||||
|
}
|
||||||
|
): EditChange[] {
|
||||||
|
const changes: EditChange[] = [];
|
||||||
|
pushChange(changes, 'Name', before.name, after.name);
|
||||||
|
pushTranslationChanges(changes, before.translations, after.translations, ['name']);
|
||||||
|
if (definition.hasItemDrop) {
|
||||||
|
pushChange(changes, 'Has item drop', boolValue(Boolean(before.hasItemDrop)), boolValue(after.hasItemDrop));
|
||||||
|
}
|
||||||
|
if (definition.hasDefault) {
|
||||||
|
pushChange(changes, 'Default category', boolValue(Boolean(before.isDefault)), boolValue(after.isDefault));
|
||||||
|
}
|
||||||
|
if (definition.hasRateable) {
|
||||||
|
pushChange(changes, 'Rateable', boolValue(Boolean(before.isRateable)), boolValue(after.isRateable));
|
||||||
|
}
|
||||||
|
if (definition.hasChangeLog) {
|
||||||
|
pushChange(changes, 'ChangeLog', before.changeLog, after.changeLog);
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
function getEditHistory(entityType: string, entityId: number): Promise<EditHistoryEntry[]> {
|
function getEditHistory(entityType: string, entityId: number): Promise<EditHistoryEntry[]> {
|
||||||
return query(
|
return query(
|
||||||
`
|
`
|
||||||
@@ -2184,6 +2296,7 @@ export async function updateDailyChecklistItem(
|
|||||||
locale = defaultLocale
|
locale = defaultLocale
|
||||||
) {
|
) {
|
||||||
const cleanPayload = cleanDailyChecklistPayload(payload);
|
const cleanPayload = cleanDailyChecklistPayload(payload);
|
||||||
|
const before = await getDailyChecklistItemById(id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
const result = await client.query(
|
const result = await client.query(
|
||||||
@@ -2200,7 +2313,8 @@ export async function updateDailyChecklistItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await replaceEntityTranslations(client, 'daily-checklist-items', id, cleanPayload.translations, ['title']);
|
await replaceEntityTranslations(client, 'daily-checklist-items', id, cleanPayload.translations, ['title']);
|
||||||
await recordEditLog(client, 'daily-checklist-items', id, 'update', userId);
|
const changes = before ? dailyChecklistEditChanges(before as DailyChecklistChangeSource, cleanPayload) : [];
|
||||||
|
await recordEditLog(client, 'daily-checklist-items', id, 'update', userId, changes);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2214,8 +2328,8 @@ export async function reorderDailyChecklistItems(payload: Record<string, unknown
|
|||||||
}
|
}
|
||||||
|
|
||||||
await withTransaction(async (client) => {
|
await withTransaction(async (client) => {
|
||||||
const existing = await client.query<{ id: number }>(
|
const existing = await client.query<{ id: number; sortOrder: number }>(
|
||||||
'SELECT id FROM daily_checklist_items WHERE id = ANY($1::integer[])',
|
'SELECT id, sort_order AS "sortOrder" FROM daily_checklist_items WHERE id = ANY($1::integer[])',
|
||||||
[ids]
|
[ids]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2223,16 +2337,25 @@ export async function reorderDailyChecklistItems(payload: Record<string, unknown
|
|||||||
throw validationError('server.validation.taskDoesNotExist');
|
throw validationError('server.validation.taskDoesNotExist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortOrders = new Map(existing.rows.map((row) => [row.id, row.sortOrder]));
|
||||||
for (const [index, id] of ids.entries()) {
|
for (const [index, id] of ids.entries()) {
|
||||||
|
const nextSortOrder = (index + 1) * 10;
|
||||||
|
const previousSortOrder = sortOrders.get(id);
|
||||||
|
if (previousSortOrder === nextSortOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
UPDATE daily_checklist_items
|
UPDATE daily_checklist_items
|
||||||
SET sort_order = $1, updated_by_user_id = $2, updated_at = now()
|
SET sort_order = $1, updated_by_user_id = $2, updated_at = now()
|
||||||
WHERE id = $3
|
WHERE id = $3
|
||||||
`,
|
`,
|
||||||
[(index + 1) * 10, userId, id]
|
[nextSortOrder, userId, id]
|
||||||
);
|
);
|
||||||
await recordEditLog(client, 'daily-checklist-items', id, 'update', userId);
|
const changes: EditChange[] = [];
|
||||||
|
pushChange(changes, 'Sort order', String(previousSortOrder), String(nextSortOrder));
|
||||||
|
await recordEditLog(client, 'daily-checklist-items', id, 'update', userId, changes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4193,6 +4316,7 @@ export async function updateConfig(
|
|||||||
const isDefault = definition.hasDefault ? Boolean(payload.isDefault) : false;
|
const isDefault = definition.hasDefault ? Boolean(payload.isDefault) : false;
|
||||||
const isRateable = definition.hasRateable ? Boolean(payload.isRateable) : false;
|
const isRateable = definition.hasRateable ? Boolean(payload.isRateable) : false;
|
||||||
const changeLog = definition.hasChangeLog ? cleanOptionalText(payload.changeLog) : '';
|
const changeLog = definition.hasChangeLog ? cleanOptionalText(payload.changeLog) : '';
|
||||||
|
const before = await getConfigById(type, id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
if (definition.hasDefault && isDefault) {
|
if (definition.hasDefault && isDefault) {
|
||||||
@@ -4241,7 +4365,10 @@ export async function updateConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await replaceEntityTranslations(client, definition.entityType, id, translations, ['name']);
|
await replaceEntityTranslations(client, definition.entityType, id, translations, ['name']);
|
||||||
await recordEditLog(client, type, id, 'update', userId);
|
const changes = before
|
||||||
|
? configEditChanges(definition, before as ConfigChangeSource, { name, translations, hasItemDrop, isDefault, isRateable, changeLog })
|
||||||
|
: [];
|
||||||
|
await recordEditLog(client, type, id, 'update', userId, changes);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const changeLabelKeys: Record<string, string> = {
|
|||||||
Name: 'common.name',
|
Name: 'common.name',
|
||||||
名字: 'common.name',
|
名字: 'common.name',
|
||||||
名称: 'common.name',
|
名称: 'common.name',
|
||||||
|
Title: 'pages.checklist.task',
|
||||||
|
标题: 'pages.checklist.task',
|
||||||
'Pokemon ID': 'pages.pokemon.id',
|
'Pokemon ID': 'pages.pokemon.id',
|
||||||
'Event item': 'common.eventItem',
|
'Event item': 'common.eventItem',
|
||||||
Genus: 'pages.pokemon.genus',
|
Genus: 'pages.pokemon.genus',
|
||||||
@@ -62,7 +64,16 @@ const changeLabelKeys: Record<string, string> = {
|
|||||||
Item: 'pages.recipes.item',
|
Item: 'pages.recipes.item',
|
||||||
物品: 'pages.recipes.item',
|
物品: 'pages.recipes.item',
|
||||||
Materials: 'pages.recipes.materials',
|
Materials: 'pages.recipes.materials',
|
||||||
需要材料: 'pages.recipes.materials'
|
需要材料: 'pages.recipes.materials',
|
||||||
|
'Sort order': 'pages.admin.sortOrder',
|
||||||
|
排序: 'pages.admin.sortOrder',
|
||||||
|
'Has item drop': 'pages.admin.hasItemDrop',
|
||||||
|
有掉落物: 'pages.admin.hasItemDrop',
|
||||||
|
'Default category': 'pages.admin.defaultCategory',
|
||||||
|
默认分类: 'pages.admin.defaultCategory',
|
||||||
|
Rateable: 'pages.admin.rateableCategory',
|
||||||
|
可评分: 'pages.admin.rateableCategory',
|
||||||
|
ChangeLog: 'pages.admin.changeLog'
|
||||||
};
|
};
|
||||||
|
|
||||||
function displayName(user: UserSummary | null): string {
|
function displayName(user: UserSummary | null): string {
|
||||||
@@ -78,6 +89,14 @@ function actionMark(action: EditHistoryAction): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function changeLabel(label: string): string {
|
function changeLabel(label: string): string {
|
||||||
|
const localizedFieldMatch = label.match(/^(.+) \(([^()]+)\)$/);
|
||||||
|
if (localizedFieldMatch) {
|
||||||
|
const [, fieldLabel, languageCode] = localizedFieldMatch;
|
||||||
|
if (fieldLabel && languageCode) {
|
||||||
|
return `${changeLabel(fieldLabel)} (${languageCode})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const key = changeLabelKeys[label];
|
const key = changeLabelKeys[label];
|
||||||
return key ? t(key) : label;
|
return key ? t(key) : label;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user