Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(date): Add Intl-Based Tests for Date Definitions to Ensure Completeness of Weekdays and Months #2912

Open
wants to merge 27 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3097485
test(date): create Intl based tests for date definitions
sossost May 18, 2024
4d3055f
test: create Intl based test for date definition
sossost May 18, 2024
fe8ce70
test : add condition filtering valid Itnl locales
sossost May 19, 2024
fa73f0a
chore : add generate:date command
sossost May 19, 2024
efb206f
feat: auto generate/update date files
sossost May 19, 2024
49f08f6
fix : Add condition that exception case 'vd'
sossost May 19, 2024
1e67eb6
Merge branch 'next' into test/create-Intl-based-tests-for-date-defini…
ST-DDT May 19, 2024
9a07a19
test : restore existing test and remove month, weekday test about cou…
sossost May 19, 2024
16f9994
Merge branch 'test/create-Intl-based-tests-for-date-definitions' of h…
sossost May 19, 2024
5005fcb
test : Refactoring to locale data instead of using a new Faker instance
sossost May 19, 2024
80ee22a
refactor : integrate function that update month, weekDay locale data …
sossost May 19, 2024
aff49d5
fix : rollback generate scripts
sossost May 19, 2024
7263f0d
docs : add comments to explain why 'dv' locale is excluded
sossost May 19, 2024
3488e11
refactor : refactor generate-locales.ts reflecting code review
ynnsuis May 20, 2024
cb80f8d
chore: review and reduce diff
ST-DDT May 20, 2024
8f92366
chore: generate files
ST-DDT May 20, 2024
2a84b19
fix: Replace underscores with dashes in Intl.DateTimeFormat locale
sossost May 23, 2024
9ccf7dc
feat: Skip generating locale date files if identical to parent
sossost May 23, 2024
c6ffe2a
Merge branch 'faker-js:next' into test/create-Intl-based-tests-for-da…
sossost May 24, 2024
7ab5dd2
refactor : Move invalidLocale function inside generateDateModule and …
sossost May 24, 2024
18d9975
Merge branch 'next' of https://github.com/sossost/faker into test/cre…
sossost May 24, 2024
08f49ef
Merge branch 'test/create-Intl-based-tests-for-date-definitions' of h…
sossost May 24, 2024
93f6dbb
Merge branch 'next' into test/create-Intl-based-tests-for-date-defini…
ST-DDT May 27, 2024
3e9d64e
Merge branch 'next' into pr/sossost/2912
ST-DDT Jun 18, 2024
a09f70d
chore: generate files only when needed
ST-DDT Jun 18, 2024
aeb62d6
chore: generate missing files
ST-DDT Jun 18, 2024
78c7183
fix: parentIntl detection
ST-DDT Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 213 additions & 31 deletions scripts/generate-locales.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#!/usr/bin/env node

/* eslint-disable no-restricted-globals */

/**
* This file contains a script that can be used to update the following files:
*
* - `src/locale/<locale>.ts`
* - `src/locales/<locale>/date/month.ts`
* - `src/locales/<locale>/date/weekday.ts`
* - `src/locales/<locale>/index.ts`
* - `src/locales/<locale>/<module...>/index.ts`
* - `src/docs/guide/localization.md`
Expand All @@ -15,10 +19,21 @@
* Run this script using `pnpm run generate:locales`
*/
import { constants } from 'node:fs';
import { access, readFile, readdir, stat, writeFile } from 'node:fs/promises';
import {
access,
mkdir,
readFile,
readdir,
stat,
writeFile,
} from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { LocaleDefinition, MetadataDefinition } from '../src/definitions';
import type {
DateEntryDefinition,
LocaleDefinition,
MetadataDefinition,
} from '../src/definitions';
import { keys } from '../src/internal/keys';
import { formatMarkdown, formatTypescript } from './apidocs/utils/format';

Expand Down Expand Up @@ -111,17 +126,48 @@ function escapeField(parent: string, module: string): string {
return module;
}

function normalizeDataRecursive<T>(localeData: T): T {
if (typeof localeData !== 'object' || localeData === null) {
// we can only traverse object-like structs
return localeData;
}

if (Array.isArray(localeData)) {
return (
[...new Set(localeData)]
// limit entries to 1k
.slice(0, 1000)
// sort entries alphabetically
.sort() as T
);
}

const result = {} as T;
for (const key of keys(localeData)) {
result[key] = normalizeDataRecursive(localeData[key]);
}

return result;
}

async function exists(path: string): Promise<boolean> {
try {
await access(path, constants.R_OK);
return true;
} catch {
// file is missing
return false;
}
}

async function generateLocaleFile(locale: string): Promise<void> {
const parts = locale.split('_');
const locales = [locale];

for (let i = parts.length - 1; i > 0; i--) {
const fallback = parts.slice(0, i).join('_');
try {
await access(resolve(pathLocales, fallback), constants.R_OK);
if (await exists(resolve(pathLocales, fallback))) {
locales.push(fallback);
} catch {
// file is missing
}
}

Expand Down Expand Up @@ -163,6 +209,11 @@ async function generateLocalesIndexFile(
modules = modules.filter((file) => !file.startsWith('.'));
modules = removeIndexTs(modules);
modules = removeTsSuffix(modules);

if (modules.length === 0) {
return;
}

modules.sort();

const content = [autoGeneratedCommentHeader];
Expand Down Expand Up @@ -286,31 +337,6 @@ async function updateLocaleFileHook(
* @param definitionKey The definition key of the current file (ex. 'location').
*/
async function normalizeLocaleFile(filePath: string, definitionKey: string) {
function normalizeDataRecursive<T>(localeData: T): T {
if (typeof localeData !== 'object' || localeData === null) {
// we can only traverse object-like structs
return localeData;
}

if (Array.isArray(localeData)) {
return (
[...new Set(localeData)]
// limit entries to 1k
.slice(0, 1000)
// sort entries alphabetically
// We cannot sort the entries locale aware as the sort order is not stable within node versions #2905
.sort() as T
);
}

const result = {} as T;
for (const key of keys(localeData)) {
result[key] = normalizeDataRecursive(localeData[key]);
}

return result;
}

const legacyDefinitions = ['app', 'cell_phone', 'team'];
const definitionsToSkip = [
'internet',
Expand Down Expand Up @@ -367,6 +393,159 @@ async function normalizeLocaleFile(filePath: string, definitionKey: string) {
return writeFile(filePath, await formatTypescript(newContent));
}

// `src/locales/<locale>/date/*
async function generateDateModule(locale: string): Promise<unknown> {
const intlLocale = locale.replaceAll('_', '-');
function isValidLocale(intlLocale: string): boolean {
// 'dv' (Dhivehi) locale is excluded because it may not be fully supported
if (locale === 'dv') return false;
try {
new Intl.DateTimeFormat(intlLocale);
return true;
} catch {
return false;
}
}

if (!isValidLocale(intlLocale)) return;

const pathDate = resolve(pathLocales, locale, 'date');
const parentLocale = locale.substring(0, locale.lastIndexOf('_')) || 'base';
const parentIntlLocale = parentLocale.replaceAll('_', '-');
const parentUsable =
isValidLocale(parentIntlLocale) &&
(await exists(resolve(pathLocales, parentLocale)));

// `src/locales/<locale>/date/weekday.ts`
async function generateWeekdayFile(): Promise<void> {
function generateWeekdays(intlLocale: string): {
wide: string[];
abbr: string[];
} {
const wide: string[] = [];
const abbr: string[] = [];
const intlWeekdayLong = new Intl.DateTimeFormat(intlLocale, {
weekday: 'long',
});
const intlWeekdayShort = new Intl.DateTimeFormat(intlLocale, {
weekday: 'short',
});
for (let i = 0; i < 7; i++) {
const date: Date = new Date(2020, 0, i + 4);
// January 4-10, 2020 are Sunday to Saturday
wide.push(intlWeekdayLong.format(date));
abbr.push(intlWeekdayShort.format(date));
}

return { wide: wide.toSorted(), abbr: abbr.toSorted() };
}

const weekdayPath = resolve(pathDate, 'weekday.ts');
let storedWeekdays: Partial<DateEntryDefinition> = {};

// Import the current weekday values
try {
const imported = await import(`file:${weekdayPath}`);
storedWeekdays = imported.default;
} catch {
// File does not exist yet
}

// Generate correct weekday values
const { wide, abbr } = generateWeekdays(intlLocale);

// Skip creation if it has the same data as the parent locale, unless the file exists already
if (parentUsable && Object.keys(storedWeekdays).length === 0) {
const parentWeekdays = generateWeekdays(parentIntlLocale);
if (JSON.stringify(parentWeekdays) === JSON.stringify({ wide, abbr })) {
return;
}
}

storedWeekdays.wide = wide;
storedWeekdays.abbr = abbr;

// Write updated values back to the file
const normalizedWeekdays = normalizeDataRecursive(storedWeekdays);
const updatedContent = `
${autoGeneratedCommentHeader}
export default ${JSON.stringify(normalizedWeekdays, null, 2)};
`;
await mkdir(pathDate, { recursive: true });
return writeFile(weekdayPath, await formatTypescript(updatedContent));
}

// `src/locales/<locale>/date/month.ts`
async function generateMonthFile(): Promise<void> {
function generateMonths(intlLocale: string): {
wide: string[];
abbr: string[];
} {
const wide: string[] = [];
const abbr: string[] = [];
const intlMonthLong = new Intl.DateTimeFormat(intlLocale, {
month: 'long',
});
const intlMonthShort = new Intl.DateTimeFormat(intlLocale, {
month: 'short',
});
for (let i = 0; i < 12; i++) {
const date: Date = new Date(2020, i, 1);
wide.push(intlMonthLong.format(date));
abbr.push(intlMonthShort.format(date));
}

return { wide: wide.toSorted(), abbr: abbr.toSorted() };
}

const monthPath = resolve(pathDate, 'month.ts');
let storedMonths: Partial<DateEntryDefinition> = {};

// Import the current month values
try {
const imported = await import(`file:${monthPath}`);
storedMonths = imported.default;
} catch {
// File does not exist yet
}

// Generate correct month values
const { wide, abbr } = generateMonths(intlLocale);

// Skip creation if it has the same data as the parent locale, unless the file exists already
if (parentUsable && Object.keys(storedMonths).length === 0) {
const parentMonths = generateMonths(parentIntlLocale);
if (JSON.stringify(parentMonths) === JSON.stringify({ wide, abbr })) {
return;
}
}

storedMonths.wide = wide;
storedMonths.abbr = abbr;

// Write updated values back to the file
const normalizedMonths = normalizeDataRecursive(storedMonths);
const updatedContent = `
${autoGeneratedCommentHeader}
export default ${JSON.stringify(normalizedMonths, null, 2)};
`;
await mkdir(pathDate, { recursive: true });
return writeFile(monthPath, await formatTypescript(updatedContent));
}

await generateWeekdayFile();
await generateMonthFile();

if (await exists(pathDate)) {
return generateRecursiveModuleIndexes(
pathDate,
locale,
'DateDefinition',
2
);
}
}

// Start of actual logic

const locales = await readdir(pathLocales);
Expand Down Expand Up @@ -410,6 +589,9 @@ for (const locale of locales) {
localesIndexImports += `import { default as ${locale} } from './${locale}';\n`;
localizationLocales += `| \`${locale}\` | ${localeTitle} | \`${localizedFaker}\` |\n`;

// We first need to generate the date module
await generateDateModule(locale);

promises.push(
// src/locale/<locale>.ts
// eslint-disable-next-line unicorn/prefer-top-level-await -- Disabled for performance
Expand Down
14 changes: 14 additions & 0 deletions src/locales/af_ZA/date/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* This file is automatically generated.
* Run 'pnpm run generate:locales' to update.
*/
import type { DateDefinition } from '../../..';
import month from './month';
import weekday from './weekday';

const date: DateDefinition = {
month,
weekday,
};

export default date;
34 changes: 34 additions & 0 deletions src/locales/af_ZA/date/month.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* This file is automatically generated.
* Run 'pnpm run generate:locales' to update.
*/
export default {
wide: [
'April',
'Augustus',
'Desember',
'Februarie',
'Januarie',
'Julie',
'Junie',
'Maart',
'Mei',
'November',
'Oktober',
'September',
],
abbr: [
'Apr.',
'Aug.',
'Des.',
'Feb.',
'Jan.',
'Jul.',
'Jun.',
'Mei',
'Mrt.',
'Nov.',
'Okt.',
'Sep.',
],
};
16 changes: 16 additions & 0 deletions src/locales/af_ZA/date/weekday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* This file is automatically generated.
* Run 'pnpm run generate:locales' to update.
*/
export default {
wide: [
'Dinsdag',
'Donderdag',
'Maandag',
'Saterdag',
'Sondag',
'Vrydag',
'Woensdag',
],
abbr: ['Di.', 'Do.', 'Ma.', 'Sa.', 'So.', 'Vr.', 'Wo.'],
};
2 changes: 2 additions & 0 deletions src/locales/af_ZA/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { LocaleDefinition } from '../..';
import cell_phone from './cell_phone';
import company from './company';
import date from './date';
import internet from './internet';
import location from './location';
import metadata from './metadata';
Expand All @@ -14,6 +15,7 @@ import phone_number from './phone_number';
const af_ZA: LocaleDefinition = {
cell_phone,
company,
date,
internet,
location,
metadata,
Expand Down
Loading