Skip to content

Feature/add support to skip entries#29

Closed
ArkeBcacy wants to merge 30 commits intomainfrom
feature/add-support-to-skip-entries
Closed

Feature/add support to skip entries#29
ArkeBcacy wants to merge 30 commits intomainfrom
feature/add-support-to-skip-entries

Conversation

@ArkeBcacy
Copy link
Copy Markdown

Added support to allow inclusion/exclusion of entries during pull/push commands.

Dependent on PR (#27)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive support for multi-language entries and entry filtering to Beacon. The changes build upon PR #27 to enable locale-specific entry versioning using a filename-based convention (e.g., Entry.en-us.yaml, Entry.fr.yaml). Additionally, it introduces include/exclude filters for entries, allowing users to selectively synchronize specific content types during pull and push operations.

Changes:

  • Added entries filtering configuration with include/exclude patterns similar to assets filtering
  • Implemented multi-locale entry support with backward-compatible file naming conventions
  • Added HTML RTE asset reference processing to handle embedded images
  • Enhanced UTF-16 encoding support in YAML file reading for cross-platform compatibility
  • Improved verbose flag handling to reduce noise in non-verbose mode

Reviewed changes

Copilot reviewed 53 out of 53 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
cli/src/cfg/Config.schema.yaml Added entries filter configuration schema
cli/src/cfg/transform/transformSchemaConfig.ts Added transformation logic for entries filters
cli/src/ui/Options.ts Added entries.isIncluded option to UI options
cli/src/ui/UiOptions.ts Implemented entries filter initialization
cli/src/schema/push.ts Integrated entry filtering into push operations
cli/src/schema/lib/pullModules.ts Integrated entry filtering into pull operations
cli/src/schema/entries/toFilesystem.ts Implemented multi-locale entry export with backward compatibility
cli/src/schema/entries/toContentstack.ts Implemented multi-locale entry import with unmodified entry handling
cli/src/schema/entries/lib/buildCreator.ts Added complex multi-locale creation logic with extensive error handling
cli/src/schema/entries/lib/loadEntryLocales.ts Added utility to load all locale versions of an entry
cli/src/schema/entries/indexAllFsEntries.ts Refactored to support multi-locale file detection
cli/src/cs/entries/getEntryLocales.ts Added API call to retrieve entry locale information
cli/src/cs/entries/exportEntryLocale.ts Added API call to export specific locale version
cli/src/cs/entries/import.ts Added locale parameter support
cli/src/cs/locales/getLocales.ts Added API call to retrieve stack locales
cli/src/cs/locales/ensureLocaleExists.ts Added utility to ensure locale exists before pushing
cli/src/cs/locales/addLocale.ts Added API call to create new locales
cli/src/dto/entry/lib/HtmlRteReplacer.ts Added HTML RTE asset reference processing
cli/src/dto/entry/beaconReplacer/lib/processHtmlRteAsset.ts Added HTML asset beacon replacement
cli/src/util/escapeRegex.ts Added regex escaping utility for safe pattern construction
cli/src/fs/readYaml.ts Enhanced with UTF-16 BOM detection and decoding
cli/src/schema/clear.ts Added deleteAssets flag to respect asset filters
cli/src/ui/command/clear.ts Added deleteAssets option to clear command
cli/src/ui/option/deleteAssets.ts Created deleteAssets CLI option
cli/src/schema/xfer/lib/handleUnmodified.ts Made unmodified item logging respect verbose flag
cli/src/schema/assets/lib/planPush.ts Made asset skip warnings respect verbose flag
cli/src/schema/assetFolders/lib/planPush.ts Made folder skip warnings respect verbose flag
test/integration/workflow/lib/loadEntry.ts Enhanced test helper to support multi-locale entries
doc/lessons-learned/multi-language-support.md Added comprehensive documentation on multi-locale implementation
README.md Added cookbook section for entry filtering configuration
build/lib/compileTypeScript.js Improved Windows compatibility for null exit status
cli/build/lib/copyFile.js Enhanced to ensure destination directory exists
cli/build/lib/post-tsc.js Fixed file URL handling for chmod operation

Comment on lines 21 to 663
@@ -59,17 +48,582 @@ export default function buildCreator(
};
}

async function loadLocaleVersions(entry: Entry, contentTypeUid: string) {
// entry.title is the base filename (extracted from actual files during indexing)
// Use it directly instead of sanitizing, since files may contain special chars
const baseFilename = entry.title;
const directory = schemaDirectory(contentTypeUid);
const fsLocaleVersions = await loadEntryLocales(
directory,
entry.title,
baseFilename,
);

if (fsLocaleVersions.length === 0) {
throw new Error(`No locale versions found for entry ${entry.title}.`);
}

return fsLocaleVersions;
}

async function createFirstLocale(
ctx: Ctx,
transformer: BeaconReplacer,
contentType: ContentType,
fsLocaleVersions: Awaited<ReturnType<typeof loadLocaleVersions>>,
): Promise<Entry> {
const [firstLocale] = fsLocaleVersions;

if (!firstLocale) {
throw new Error('No locale versions found');
}

const transformed = transformer.process(firstLocale.entry);

// Pass undefined for 'default' locale (single-locale backward compat)
const locale =
firstLocale.locale === 'default' ? undefined : firstLocale.locale;

try {
return await importEntry(
ctx.cs.client,
contentType.uid,
transformed,
false,
locale,
);
} catch (ex) {
return await handleDuplicateKeyError(
ex,
ctx,
contentType,
transformed,
locale,
);
}
}

async function createEntryWithGloballyUniqueTitle(
ctx: Ctx,
contentType: ContentType,
transformed: ReturnType<BeaconReplacer['process']>,
locale: string | undefined,
): Promise<Entry> {
const ui = getUi();
const uniqueTitle = `${transformed.title} (${contentType.uid})`;

ui.warn(
`Title "${transformed.title}" conflicts globally. ` +
`Creating with unique title: "${uniqueTitle}"`,
);

return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...transformed, title: uniqueTitle },
false,
locale,
);
}

async function updateExistingEntry(
ctx: Ctx,
contentType: ContentType,
transformed: ReturnType<BeaconReplacer['process']>,
uid: string,
locale: string | undefined,
): Promise<Entry> {
try {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...transformed, uid },
true,
locale,
);
} catch (updateEx) {
if (isTitleNotUniqueError(updateEx)) {
const ui = getUi();
const uniqueTitle = `${transformed.title} [${uid}]`;

ui.warn(
`Title "${transformed.title}" conflicts in ${locale ?? 'default'} locale. ` +
`Using unique title: "${uniqueTitle}"`,
);

return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...transformed, title: uniqueTitle, uid },
true,
locale,
);
}
throw updateEx;
}
}

async function handleDuplicateKeyError(
ex: unknown,
ctx: Ctx,
contentType: ContentType,
transformed: ReturnType<BeaconReplacer['process']>,
locale: string | undefined,
): Promise<Entry> {
if (isDuplicateKeyError(ex)) {
const uid = await getUidByTitle(
ctx.cs.client,
ctx.cs.globalFields,
contentType,
transformed.title,
);

if (!uid) {
return await createEntryWithGloballyUniqueTitle(
ctx,
contentType,
transformed,
locale,
);
}

return await updateExistingEntry(
ctx,
contentType,
transformed,
uid,
locale,
);
}

throw ex;
}

async function handleDuplicateKeyInLocale(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<Entry | undefined> {
const ui = getUi();
ui.warn(
`Locale ${locale} for entry ${createdUid} already exists. Updating instead.`,
);

try {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, uid: createdUid },
true, // Switch to update mode
locale,
);
} catch (updateEx) {
// If update also fails with title conflict, handle it
if (isTitleNotUniqueError(updateEx)) {
return await handleTitleNotUniqueInLocale(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);
}
throw updateEx;
}
}

async function handleLocaleImportError(
ex: unknown,
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<Entry | undefined> {
if (isLocaleAlreadyExistsError(ex)) {
return await handleLocaleExistsError(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);
}

if (isTitleNotUniqueError(ex)) {
return await handleTitleNotUniqueInLocale(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);
}

if (isDuplicateKeyError(ex)) {
return await handleDuplicateKeyInLocale(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);
}

throw ex;
}

/**
* Searches for an entry in a specific locale by title.
* This is used to find conflicting standalone entries that need to be deleted
* before creating a proper locale version.
*/
async function findEntryByTitleInLocale(
client: Client,
contentTypeUid: string,
title: string,
locale: string,
): Promise<string | undefined> {
const result = await client.GET(
'/v3/content_types/{content_type_uid}/entries',
{
params: {
path: { content_type_uid: contentTypeUid },
query: {
limit: 1,
locale,
query: JSON.stringify({ title }),
},
},
},
);

const data = result.data as unknown as
| { entries: { uid?: string }[] }
| undefined;

if (!data?.entries) {
return undefined;
}

const { entries } = data;

if (Array.isArray(entries) && entries.length > 0) {
return entries[0]?.uid;
}

return undefined;
}

async function deleteConflictAndRetry(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
conflictingUid: string,
): Promise<Entry> {
const ui = getUi();
ui.warn(
`Deleting standalone ${locale} entry "${localeTransformed.title}" (${conflictingUid}) ` +
`to create locale version of ${createdUid}`,
);

await deleteEntry(ctx.cs.client, contentType.uid, conflictingUid);

return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, uid: createdUid },
true,
locale,
);
}

async function importWithUniqueTitle(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<Entry> {
const ui = getUi();
const uniqueTitle = `${localeTransformed.title} (${contentType.uid})`;

ui.warn(
`Title "${localeTransformed.title}" conflicts globally in ${locale} locale. ` +
`Using unique title: "${uniqueTitle}"`,
);

return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, title: uniqueTitle, uid: createdUid },
true,
locale,
);
}

async function handleLocaleExistsError(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<Entry> {
try {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, uid: createdUid },
true,
locale,
);
} catch (updateEx) {
if (!isTitleNotUniqueError(updateEx)) {
throw updateEx;
}

const conflictingUid = await findEntryByTitleInLocale(
ctx.cs.client,
contentType.uid,
localeTransformed.title,
locale,
);

if (conflictingUid && conflictingUid !== createdUid) {
return await deleteConflictAndRetry(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
conflictingUid,
);
}

return await importWithUniqueTitle(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);
}
}

async function deleteConflictingEntries(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<string[]> {
const conflictingUid = await findEntryByTitleInLocale(
ctx.cs.client,
contentType.uid,
localeTransformed.title,
locale,
);

const suffixedTitle = `${localeTransformed.title} (${contentType.uid})`;
const conflictingSuffixedUid = await findEntryByTitleInLocale(
ctx.cs.client,
contentType.uid,
suffixedTitle,
locale,
);

const uidsToDelete = [conflictingUid, conflictingSuffixedUid].filter(
(uid): uid is string => Boolean(uid) && uid !== createdUid,
);

if (uidsToDelete.length > 0) {
const ui = getUi();
for (const uid of uidsToDelete) {
ui.warn(
`Deleting standalone ${locale} entry (${uid}) to create locale version of ${createdUid}`,
);
await deleteEntry(ctx.cs.client, contentType.uid, uid);
}
}

return uidsToDelete;
}

async function importWithTimestampedTitle(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<Entry> {
const ui = getUi();
const timestamp = Date.now();
const uniqueTitle = `${localeTransformed.title} [${timestamp}]`;

ui.warn(
`Title "${localeTransformed.title}" conflicts in ${locale} locale. ` +
`Using unique title with timestamp: "${uniqueTitle}"`,
);

try {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, title: uniqueTitle, uid: createdUid },
true,
locale,
);
} catch (createEx) {
if (isLocaleAlreadyExistsError(createEx)) {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, title: uniqueTitle, uid: createdUid },
true,
locale,
);
}
throw createEx;
}
}

async function handleTitleNotUniqueInLocale(
ctx: Ctx,
contentType: ContentType,
localeTransformed: ReturnType<BeaconReplacer['process']>,
createdUid: string,
locale: string,
): Promise<Entry> {
const uidsDeleted = await deleteConflictingEntries(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);

if (uidsDeleted.length > 0) {
try {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, uid: createdUid },
true,
locale,
);
} catch (retryEx) {
if (!isTitleNotUniqueError(retryEx)) {
throw retryEx;
}
}
}

return await importWithTimestampedTitle(
ctx,
contentType,
localeTransformed,
createdUid,
locale,
);
}

async function importLocaleVersion(
ctx: Ctx,
transformer: BeaconReplacer,
contentType: ContentType,
localeVersion: { locale: string; entry: Entry },
createdUid: string,
): Promise<Entry | undefined> {
if (localeVersion.locale === 'default') {
return;
}

const localeTransformed = transformer.process(localeVersion.entry);

// For locale versions, always use overwrite mode because we're adding/updating
// a locale on an existing entry (created in the base language).
// Contentstack requires overwrite=true when creating locale versions.
try {
return await importEntry(
ctx.cs.client,
contentType.uid,
{ ...localeTransformed, uid: createdUid },
true, // Always use overwrite mode for locale versions
localeVersion.locale,
);
} catch (ex) {
return await handleLocaleImportError(
ex,
ctx,
contentType,
localeTransformed,
createdUid,
localeVersion.locale,
);
}
}

async function importAdditionalLocales(
ctx: Ctx,
transformer: BeaconReplacer,
contentType: ContentType,
fsLocaleVersions: Awaited<ReturnType<typeof loadLocaleVersions>>,
created: Entry,
) {
const importPromises = fsLocaleVersions
.slice(1)
.map(async (localeVersion) =>
importLocaleVersion(
ctx,
transformer,
contentType,
localeVersion,
created.uid,
),
);

await Promise.all(importPromises);
}

function isLocaleAlreadyExistsError(ex: unknown): boolean {
// Error code 201: Entry already exists in locale
return (
ex instanceof ContentstackError &&
ex.code === ERROR_CODE_ENTRY_ALREADY_EXISTS
);
}

function isTitleNotUniqueError(ex: unknown): boolean {
// Error code 119: Entry import failed with "title is not unique"
if (
!(ex instanceof ContentstackError) ||
ex.code !== ERROR_CODE_TITLE_NOT_UNIQUE
) {
return false;
}

return isDeepStrictEqual(ex.details, { title: ['is not unique.'] });
}

function isDuplicateKeyError(ex: unknown) {
if (!(ex instanceof ContentstackError)) {
return false;
}

const invalidDataCode = 119;
if (ex.code !== invalidDataCode) {
return false;
if (ex.code === ERROR_CODE_TITLE_NOT_UNIQUE) {
return isDeepStrictEqual(ex.details, { title: ['is not unique.'] });
}

return isDeepStrictEqual(ex.details, { title: ['is not unique.'] });
if (ex.code === ERROR_CODE_ENTRY_ALREADY_EXISTS) {
// Code 201 means entry already exists in the specified locale
return true;
}

return false;
}

async function getUidByTitle(
@@ -79,16 +633,31 @@ async function getUidByTitle(
title: string,
) {
const entries = await indexEntries(client, globalFieldsByUid, contentType);
return entries.get(title)?.uid;
}

function logInvalidState(contentTypeTitle: string, entryTitle: string) {
const y = createStylus('yellowBright');
const ui = getUi();
const msg1 = y`While importing ${contentTypeTitle} entry ${entryTitle},`;
const msg2 = 'Contentstack reported a duplicate key error based on the';
const msg3 = 'title, but no entry with that title was found after';
const msg4 = 're-indexing.';
const msg = [msg1, msg2, msg3, msg4].join(' ');
ui.warn(msg);
// Try exact title first
let uid = entries.get(title)?.uid;
if (uid) {
return uid;
}

// Try trimmed title if exact match fails (handles trailing/leading spaces)
const trimmedTitle = title.trim();
if (trimmedTitle !== title) {
uid = entries.get(trimmedTitle)?.uid;
if (uid) {
return uid;
}
}

// Try with content type suffix appended (handles entries created before UIDs were added)
// Previous Beacon pushes without UIDs would create entries with title conflicts
// and append the content type UID to make them unique
const titleWithSuffix = `${title} (${contentType.uid})`;
uid = entries.get(titleWithSuffix)?.uid;
if (uid) {
return uid;
}

// No match found
return undefined;
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buildCreator.ts file (664 lines) contains complex multi-locale creation logic with extensive error handling for title conflicts, duplicate entries, and locale version management, but lacks test coverage. This file includes critical functions like createFirstLocale, importAdditionalLocales, handleDuplicateKeyError, handleLocaleImportError, and multiple conflict resolution strategies. Add comprehensive unit tests to verify behavior for success paths, error paths, and edge cases such as: locale already exists errors, title uniqueness conflicts, duplicate key scenarios, and various Contentstack API error codes (119, 201).

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +17
export default function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escapeRegex utility function lacks test coverage. This function is critical for safely constructing regex patterns from user-controlled filenames (which may contain special regex characters like dots, brackets, etc.). Add a test file at cli/src/util/escapeRegex.test.ts to verify that special regex characters are properly escaped.

Copilot uses AI. Check for mistakes.
const result = {
...(assets ? { assets: { isIncluded: compileFilters(assets) } } : {}),
...(strategy ? { deletionStrategy: strategy } : {}),
...(entries ? { entries: { isIncluded: compileFilters(entries) } } : {}),
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entries filtering configuration lacks test coverage. Similar to how assets filtering is tested in transformSchemaConfig.test.ts, add tests to verify that entries include/exclude patterns are correctly compiled and applied. This ensures the configuration transformation works correctly for the new entries filter feature.

Copilot uses AI. Check for mistakes.
Comment on lines 63 to +240
@@ -49,38 +99,142 @@ export default async function toContentstack(
throw new Error(`No matching entry found for ${title}.`);
}

// Load all locale versions from filesystem
const fsLocaleVersions = await loadFsLocaleVersions(
fs,
contentType.uid,
filenamesByTitle,
);

const csLocaleSet = await getExistingLocales(ctx, contentType, cs.uid);

// Only push if there are new locale versions to sync
if (!shouldSyncLocales(fsLocaleVersions, csLocaleSet)) {
continue;
}

// Push all locale versions (including new locales like zh-cn)
await updateAllLocales(
ctx,
transformer,
contentType,
fsLocaleVersions,
cs.uid,
csLocaleSet,
);

const entry = { ...fs, uid: cs.uid };
ctx.references.recordEntryForReferences(contentType.uid, entry);
}

return result;
}

function buildUpdateFn(
ctx: Ctx,
csEntriesByTitle: ReadonlyMap<string, Entry>,
transformer: BeaconReplacer,
contentType: ContentType,
filenamesByTitle: ReadonlyMap<Entry['uid'], string>,
) {
return async (entry: Entry) => {
const match = csEntriesByTitle.get(entry.title);

if (!match) {
throw new Error(`No matching entry found for ${entry.title}.`);
}

const transformed = transformer.process(entry);

const updated = await importEntry(
ctx.cs.client,
const fsLocaleVersions = await loadFsLocaleVersions(
entry,
contentType.uid,
{ ...transformed, uid: match.uid },
true,
filenamesByTitle,
);

const csLocaleSet = await getExistingLocales(ctx, contentType, match.uid);

await updateAllLocales(
ctx,
transformer,
contentType,
fsLocaleVersions,
match.uid,
csLocaleSet,
);

ctx.references.recordEntryForReferences(contentType.uid, {
...entry,
uid: updated.uid,
uid: match.uid,
});
};
}

async function loadFsLocaleVersions(
entry: Entry,
contentTypeUid: string,
filenamesByTitle: ReadonlyMap<Entry['uid'], string>,
) {
const filename = filenamesByTitle.get(entry.title);
if (!filename) {
throw new Error(`No filename found for entry ${entry.title}.`);
}

const baseFilename = filename.replace(/\.yaml$/u, '');
const directory = schemaDirectory(contentTypeUid);

return loadEntryLocales(directory, entry.title, baseFilename);
}

async function getExistingLocales(
ctx: Ctx,
contentType: ContentType,
entryUid: string,
): Promise<Set<string>> {
try {
const csLocales = await getEntryLocales(
ctx.cs.client,
contentType.uid,
entryUid,
);
return new Set(csLocales.map((l) => l.code));
} catch {
// If the locales endpoint fails (e.g., not supported by Contentstack instance
// or entry doesn't exist yet), return empty set to indicate no locales are known
return new Set<string>();
}
}

async function updateAllLocales(
ctx: Ctx,
transformer: BeaconReplacer,
contentType: ContentType,
fsLocaleVersions: Awaited<ReturnType<typeof loadFsLocaleVersions>>,
entryUid: string,
csLocaleSet: Set<string>,
) {
// Ensure all required locales exist in the target stack before pushing entries
const localeEnsurePromises = fsLocaleVersions
.filter((lv) => lv.locale !== 'default')
.map(async (lv) => ensureLocaleExists(ctx.cs.client, lv.locale));

await Promise.all(localeEnsurePromises);

// Import all locale versions in parallel for better performance
const importPromises = fsLocaleVersions.map(async (localeVersion) => {
const transformed = transformer.process(localeVersion.entry);

// Pass undefined for 'default' locale (single-locale backward compat)
const locale =
localeVersion.locale === 'default' ? undefined : localeVersion.locale;

// Always use overwrite=true for locale-specific versions since the entry exists.
// For 'default' locale (single-locale backward compat), only overwrite if entry has locales.
const overwrite = locale ? true : csLocaleSet.size > 0;

return importEntry(
ctx.cs.client,
contentType.uid,
{ ...transformed, uid: entryUid },
overwrite,
locale,
);
});

await Promise.all(importPromises);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The helper functions shouldSyncLocales, processUnmodifiedEntries, loadFsLocaleVersions, getExistingLocales, and updateAllLocales lack test coverage. These functions handle critical logic for multi-locale synchronization including determining when to sync locales, loading filesystem locale versions, and coordinating parallel imports. Add unit tests to verify their behavior, especially edge cases like empty locale lists, network failures, and race conditions.

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 42
export default async function readYaml(pathLike: PathLike): Promise<unknown> {
const raw = await readFile(pathLike, 'utf-8');
// Read as buffer first to detect encoding
const buffer = await readFile(pathLike);

// Check for UTF-16 LE BOM (FF FE)
let raw: string;
if (buffer[0] === UTF16_LE_BOM_BYTE1 && buffer[1] === UTF16_LE_BOM_BYTE2) {
raw = buffer.toString('utf16le');
}
// Check for UTF-16 BE BOM (FE FF)
else if (
buffer[0] === UTF16_BE_BOM_BYTE1 &&
buffer[1] === UTF16_BE_BOM_BYTE2
) {
// Note: Node.js doesn't have built-in utf16be, so we swap bytes
const swapped = Buffer.alloc(buffer.length);
for (let i = 0; i < buffer.length; i += BYTES_PER_UTF16_CHAR) {
const byte1 = buffer[i + 1];
const byte2 = buffer[i];
if (byte1 !== undefined) swapped[i] = byte1;
if (byte2 !== undefined) swapped[i + 1] = byte2;
}
raw = swapped.toString('utf16le');
}
// Default to UTF-8
else {
raw = buffer.toString('utf-8');
}

return parse(raw);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UTF-16 encoding detection logic lacks test coverage. This is critical functionality that handles edge cases like different byte order marks and byte swapping for big-endian encoding. Add tests to verify correct behavior for UTF-16 LE, UTF-16 BE, and UTF-8 files, including empty buffers and odd-length buffers.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to 54
function isValidLocaleCode(code: string): boolean {
// Locale codes: 2-3 letter language code, optionally followed by separator and 2-4 letter region code
// Pattern matches: en, en-us, en-US, fr-CA, zh_CN, etc.
return /^[a-z]{2,3}(?:[_-][a-z]{2,4})?$/iu.test(code);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isValidLocaleCode function is duplicated in multiple locations (test/integration/workflow/lib/loadEntry.ts line 50 and cli/src/schema/entries/indexAllFsEntries.ts line 161). Consider extracting this into a shared utility function to avoid code duplication and ensure consistent locale validation across the codebase.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +90
// If only one locale, save without locale suffix for backward compatibility.
// When multiple locales exist, write English as base file and other locales
// with locale suffixes (e.g., Entry.yaml for English, Entry.zh-chs.yaml for Chinese)

// Write all locale versions in parallel for better performance
const writePromises = locales.map(async (locale) => {
const isEnglish = /^en(?:[-_]|$)/iu.test(locale.code);
// Use locale suffix for non-English locales when multiple locales exist
const useLocaleSuffix = locales.length > 1 && !isEnglish;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a discrepancy between the documentation and the implementation. The documentation (line 43) states: "If an entry has multiple locale versions, all are saved with locale suffixes". However, the code (lines 82-90) writes English locales without a suffix even when multiple locales exist. The comment on line 83 mentions "write English as base file" but this contradicts the stated documentation. Either update the documentation to accurately reflect that English is treated specially, or modify the code to add locale suffixes to all files when multiple locales exist.

Copilot uses AI. Check for mistakes.
@ArkeBcacy ArkeBcacy closed this Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants