diff --git a/packages/x-date-pickers/src/locales/deDE.ts b/packages/x-date-pickers/src/locales/deDE.ts index 8c01d6e86a807..690708ba0fb66 100644 --- a/packages/x-date-pickers/src/locales/deDE.ts +++ b/packages/x-date-pickers/src/locales/deDE.ts @@ -1,17 +1,40 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +// import { CalendarPickerView } from '../internals/models'; const deDEPickers: Partial> = { + // Calendar navigation previousMonth: 'Letzter Monat', nextMonth: 'Nächster Monat', + + // View navigation openPreviousView: 'Letzte Ansicht öffnen', openNextView: 'Nächste Ansicht öffnen', + // calendarViewSwitchingButtonAriaLabel: (view: CalendarPickerView) => view === 'year' ? 'year view is open, switch to calendar view' : 'calendar view is open, switch to year view', + + // DateRange placeholders + start: 'Beginn', + end: 'Ende', + + // Action bar cancelButtonLabel: 'Abbrechen', clearButtonLabel: 'Löschen', okButtonLabel: 'OK', todayButtonLabel: 'Heute', - start: 'Beginn', - end: 'Ende', + + // Clock labels + // clockLabelText: (view, time, adapter) => `Select ${view}. ${time === null ? 'No time selected' : `Selected time is ${adapter.format(time, 'fullTime')}`}`, + // hoursClockNumberText: hours => `${hours} hours`, + // minutesClockNumberText: minutes => `${minutes} minutes`, + // secondsClockNumberText: seconds => `${seconds} seconds`, + + // Open picker labels + // openDatePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose date, selected date is ${utils.format(utils.date(rawValue)!, 'fullDate')}` : 'Choose date', + // openTimePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose time, selected time is ${utils.format(utils.date(rawValue)!, 'fullTime')}` : 'Choose time', + + // Table labels + // timeTableLabel: 'pick time', + // dateTableLabel: 'pick date', }; export const deDE = getPickersLocalization(deDEPickers); diff --git a/packages/x-date-pickers/src/locales/enUS.ts b/packages/x-date-pickers/src/locales/enUS.ts index 48cbb8bc1ac5f..79fe5e657b0bc 100644 --- a/packages/x-date-pickers/src/locales/enUS.ts +++ b/packages/x-date-pickers/src/locales/enUS.ts @@ -3,21 +3,31 @@ import { getPickersLocalization } from './utils/getPickersLocalization'; import { CalendarPickerView } from '../internals/models'; // This object is not Partial because it is the default values + const enUSPickers: PickersLocaleText = { + // Calendar navigation previousMonth: 'Previous month', nextMonth: 'Next month', + + // View navigation openPreviousView: 'open previous view', openNextView: 'open next view', - cancelButtonLabel: 'Cancel', - clearButtonLabel: 'Clear', - okButtonLabel: 'OK', - todayButtonLabel: 'Today', - start: 'Start', - end: 'End', calendarViewSwitchingButtonAriaLabel: (view: CalendarPickerView) => view === 'year' ? 'year view is open, switch to calendar view' : 'calendar view is open, switch to year view', + + // DateRange placeholders + start: 'Start', + end: 'End', + + // Action bar + cancelButtonLabel: 'Cancel', + clearButtonLabel: 'Clear', + okButtonLabel: 'OK', + todayButtonLabel: 'Today', + + // Clock labels clockLabelText: (view, time, adapter) => `Select ${view}. ${ time === null ? 'No time selected' : `Selected time is ${adapter.format(time, 'fullTime')}` @@ -25,6 +35,8 @@ const enUSPickers: PickersLocaleText = { hoursClockNumberText: (hours) => `${hours} hours`, minutesClockNumberText: (minutes) => `${minutes} minutes`, secondsClockNumberText: (seconds) => `${seconds} seconds`, + + // Open picker labels openDatePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose date, selected date is ${utils.format(utils.date(rawValue)!, 'fullDate')}` @@ -33,6 +45,8 @@ const enUSPickers: PickersLocaleText = { rawValue && utils.isValid(utils.date(rawValue)) ? `Choose time, selected time is ${utils.format(utils.date(rawValue)!, 'fullTime')}` : 'Choose time', + + // Table labels timeTableLabel: 'pick time', dateTableLabel: 'pick date', }; diff --git a/packages/x-date-pickers/src/locales/frFR.ts b/packages/x-date-pickers/src/locales/frFR.ts index 7049ec2d72c57..1bc9ab9ad4d48 100644 --- a/packages/x-date-pickers/src/locales/frFR.ts +++ b/packages/x-date-pickers/src/locales/frFR.ts @@ -1,17 +1,40 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +// import { CalendarPickerView } from '../internals/models'; const frFRPickers: Partial> = { + // Calendar navigation previousMonth: 'Mois précédent', nextMonth: 'Mois suivant', + + // View navigation openPreviousView: 'Ouvrir la vue précédente', openNextView: 'Ouvrir la vue suivante', + // calendarViewSwitchingButtonAriaLabel: (view: CalendarPickerView) => view === 'year' ? 'year view is open, switch to calendar view' : 'calendar view is open, switch to year view', + + // DateRange placeholders + start: 'Début', + end: 'Fin', + + // Action bar cancelButtonLabel: 'Annuler', clearButtonLabel: 'Vider', okButtonLabel: 'OK', todayButtonLabel: "Aujourd'hui", - start: 'Début', - end: 'Fin', + + // Clock labels + // clockLabelText: (view, time, adapter) => `Select ${view}. ${time === null ? 'No time selected' : `Selected time is ${adapter.format(time, 'fullTime')}`}`, + // hoursClockNumberText: hours => `${hours} hours`, + // minutesClockNumberText: minutes => `${minutes} minutes`, + // secondsClockNumberText: seconds => `${seconds} seconds`, + + // Open picker labels + // openDatePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose date, selected date is ${utils.format(utils.date(rawValue)!, 'fullDate')}` : 'Choose date', + // openTimePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose time, selected time is ${utils.format(utils.date(rawValue)!, 'fullTime')}` : 'Choose time', + + // Table labels + // timeTableLabel: 'pick time', + // dateTableLabel: 'pick date', }; export const frFR = getPickersLocalization(frFRPickers); diff --git a/packages/x-date-pickers/src/locales/trTR.ts b/packages/x-date-pickers/src/locales/trTR.ts index 17707111e66a3..30b957fc3d84b 100644 --- a/packages/x-date-pickers/src/locales/trTR.ts +++ b/packages/x-date-pickers/src/locales/trTR.ts @@ -1,18 +1,41 @@ import { PickersLocaleText } from './utils/pickersLocaleTextApi'; import { getPickersLocalization } from './utils/getPickersLocalization'; +// import { CalendarPickerView } from '../internals/models'; // This object is not Partial because it is the default values const trTRPickers: Partial> = { + // Calendar navigation previousMonth: 'Önceki ay', nextMonth: 'Sonraki ay', + + // View navigation openPreviousView: 'sonraki görünüm', openNextView: 'önceki görünüm', + // calendarViewSwitchingButtonAriaLabel: (view: CalendarPickerView) => view === 'year' ? 'year view is open, switch to calendar view' : 'calendar view is open, switch to year view', + + // DateRange placeholders + start: 'Başlangıç', + end: 'Bitiş', + + // Action bar cancelButtonLabel: 'iptal', clearButtonLabel: 'Temizle', okButtonLabel: 'Tamam', todayButtonLabel: 'Bugün', - start: 'Başlangıç', - end: 'Bitiş', + + // Clock labels + // clockLabelText: (view, time, adapter) => `Select ${view}. ${time === null ? 'No time selected' : `Selected time is ${adapter.format(time, 'fullTime')}`}`, + // hoursClockNumberText: hours => `${hours} hours`, + // minutesClockNumberText: minutes => `${minutes} minutes`, + // secondsClockNumberText: seconds => `${seconds} seconds`, + + // Open picker labels + // openDatePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose date, selected date is ${utils.format(utils.date(rawValue)!, 'fullDate')}` : 'Choose date', + // openTimePickerDialogue: (rawValue, utils) => rawValue && utils.isValid(utils.date(rawValue)) ? `Choose time, selected time is ${utils.format(utils.date(rawValue)!, 'fullTime')}` : 'Choose time', + + // Table labels + // timeTableLabel: 'pick time', + // dateTableLabel: 'pick date', }; export const trTR = getPickersLocalization(trTRPickers); diff --git a/scripts/l10n.ts b/scripts/l10n.ts index 11438f504877a..48b86cbf23d82 100644 --- a/scripts/l10n.ts +++ b/scripts/l10n.ts @@ -16,6 +16,21 @@ const GIT_REPO = 'mui-x'; const L10N_ISSUE_ID = 3211; const SOURCE_CODE_REPO = `https://github.com/${GIT_ORGANIZATION}/${GIT_REPO}`; +const packagesWithL10n = [ + { + key: 'data-grid', + reportName: '🧑‍💼 DataGrid, DataGridPro, DataGridPremium', + constantsRelativePath: 'packages/grid/x-data-grid/src/constants/localeTextConstants.ts', + localesRelativePath: 'packages/grid/x-data-grid/src/locales', + }, + { + key: 'pickers', + reportName: '📅🕒 Date and Time Pickers', + constantsRelativePath: 'packages/x-date-pickers/src/locales/enUS.ts', + localesRelativePath: 'packages/x-date-pickers/src/locales', + }, +]; + const BABEL_PLUGINS = [require.resolve('@babel/plugin-syntax-typescript')]; type Translations = Record; @@ -46,8 +61,8 @@ function plugin(existingTranslations: Translations): babel.PluginObj { return; } - // Test if the variable name follows the pattern xxXXGrid - if (!/[a-z]{2}[A-Z]{2}Grid/.test(node.id.name)) { + // Test if the variable name follows the pattern xxXXGrid or xxXXPickers + if (!/[a-z]{2}[A-Z]{2}(Grid|Pickers)/.test(node.id.name)) { visitorPath.skip(); return; } @@ -133,7 +148,7 @@ function extractTranslations(translationsPath: string): [TranslationsByGroup, Tr return [translationsByGroup, translations]; } -function findLocales(localesDirectory: string) { +function findLocales(localesDirectory: string, constantsPath: string) { const items = fse.readdirSync(localesDirectory); const locales: any[] = []; const localeRegex = /^[a-z]{2}[A-Z]{2}/; @@ -146,7 +161,10 @@ function findLocales(localesDirectory: string) { const localePath = path.resolve(localesDirectory, item); const code = match[0]; - locales.push([localePath, code]); + if (constantsPath !== localePath) { + // Ignore the locale used as a reference + locales.push([localePath, code]); + } }); return locales; @@ -201,19 +219,66 @@ function countryToFlag(isoCode: string) { : isoCode; } -async function generateReport( - missingTranslations: Record, -) { +interface MissingKey { + currentLineContent: string; + lineIndex: number; +} + +interface MissingTranslations { + [localeCode: string]: { + [packageCode: string]: { + path: string; + missingKeys: MissingKey[]; + }; + }; +} + +async function generateReport(missingTranslations: MissingTranslations) { const lastCommitRef = await git('log -n 1 --pretty="format:%H"'); const lines: string[] = []; - Object.entries(missingTranslations).forEach(([code, info]) => { - if (info.locations.length === 0) { - return; - } - lines.push(`### ${countryToFlag(code.slice(2))} ${code.slice(0, 2)}-${code.slice(2)}`); - info.locations.forEach((location) => { - const permalink = `${SOURCE_CODE_REPO}/blob/${lastCommitRef}/${info.path}#L${location}`; - lines.push(permalink); + Object.entries(missingTranslations).forEach(([languageCode, infoPerPackage]) => { + lines.push(''); + lines.push( + `### ${countryToFlag(languageCode.slice(2))} ${languageCode.slice(0, 2)}-${languageCode.slice( + 2, + )}`, + ); + + packagesWithL10n.forEach(({ key: packageKey, reportName, localesRelativePath }) => { + const info = infoPerPackage[packageKey]; + + lines.push('
'); + + const fileName = `${languageCode.slice(0, 2).toLowerCase()}${languageCode + .slice(2) + .toUpperCase()}.ts`; + const filePath = `${localesRelativePath}/${fileName}`; + if (!info) { + lines.push(` ${reportName}: file to create`); + lines.push(''); + lines.push(` > Add file \`${filePath}\` to start contributing to this locale.`); + } else if (info.missingKeys.length === 0) { + lines.push(` ${reportName} (Done ✅)`); + lines.push(''); + lines.push(` > This locale has been completed by the community 🚀`); + lines.push( + ` > You can still look for typo fix or improvements in [the translation file](${SOURCE_CODE_REPO}/blob/${lastCommitRef}/${filePath}) 🕵`, + ); + } else { + lines.push(` ${reportName} (${info.missingKeys.length} remaining)`); + lines.push(''); + info.missingKeys.forEach((missingKey) => { + const permalink = `${SOURCE_CODE_REPO}/blob/${lastCommitRef}/${info.path}#L${missingKey.lineIndex}`; + let lineContent = missingKey.currentLineContent; + + if (lineContent[lineContent.length - 1] === ',') { + lineContent = lineContent.slice(0, lineContent.length - 1); + } + lines.push(` - [ \`\` ${lineContent} \`\`](${permalink})`); + }); + } + + lines.push('
'); }); }); return lines.join('\n'); @@ -229,7 +294,6 @@ async function updateIssue(githubToken, newMessage) { Run \`yarn l10n --report\` to update the list below ⬇️ -## DataGrid / DataGridPro ${newMessage} `; await octokit @@ -256,64 +320,72 @@ async function run(argv: yargs.ArgumentsCamelCase) { const { report, githubToken } = argv; const workspaceRoot = path.resolve(__dirname, '../'); - const constantsPath = path.join( - workspaceRoot, - 'packages/grid/x-data-grid/src/constants/localeTextConstants.ts', - ); - const [baseTranslationsByGroup, baseTranslations] = extractTranslations(constantsPath); + const missingTranslations: Record = {}; - const localesDirectory = path.resolve(workspaceRoot, 'packages/grid/x-data-grid/src/locales'); - const locales = findLocales(localesDirectory); + packagesWithL10n.forEach((packageInfo) => { + const constantsPath = path.join(workspaceRoot, packageInfo.constantsRelativePath); + const [baseTranslationsByGroup, baseTranslations] = extractTranslations(constantsPath); - const missingTranslations: Record = {}; + const localesDirectory = path.resolve(workspaceRoot, packageInfo.localesRelativePath); + const locales = findLocales(localesDirectory, constantsPath); - locales.forEach(([localePath, localeCode]) => { - const { - translations: existingTranslations, - transformedCode, - rawCode, - } = extractAndReplaceTranslations(localePath); + locales.forEach(([localePath, localeCode]) => { + const { + translations: existingTranslations, + transformedCode, + rawCode, + } = extractAndReplaceTranslations(localePath); - if (!transformedCode || Object.keys(existingTranslations).length === 0) { - return; - } + if (!transformedCode || Object.keys(existingTranslations).length === 0) { + return; + } - const codeWithNewTranslations = injectTranslations( - transformedCode, - existingTranslations, - baseTranslationsByGroup, - ); + const codeWithNewTranslations = injectTranslations( + transformedCode, + existingTranslations, + baseTranslationsByGroup, + ); - const prettierConfigPath = path.join(workspaceRoot, 'prettier.config.js'); - const prettierConfig = prettier.resolveConfig.sync(localePath, { config: prettierConfigPath }); + const prettierConfigPath = path.join(workspaceRoot, 'prettier.config.js'); + const prettierConfig = prettier.resolveConfig.sync(localePath, { + config: prettierConfigPath, + }); - const prettifiedCode = prettier.format(codeWithNewTranslations, { - ...prettierConfig, - filepath: localePath, - }); + const prettifiedCode = prettier.format(codeWithNewTranslations, { + ...prettierConfig, + filepath: localePath, + }); - const lines = rawCode.split('\n'); - Object.entries(baseTranslations).forEach(([key]) => { - if (!existingTranslations[key]) { - if (!missingTranslations[localeCode]) { - missingTranslations[localeCode] = { - path: localePath.replace(workspaceRoot, '').slice(1), // Remove leading slash - locations: [], - }; - } - const location = lines.findIndex((line) => line.trim().startsWith(`// ${key}:`)); - // Ignore when both the translation and the placeholder are missing - if (location >= 0) { - missingTranslations[localeCode].locations.push(location + 1); + // We always set the `locations` to [] such that we can differentiate translation completed from un-existing translations + if (!missingTranslations[localeCode]) { + missingTranslations[localeCode] = {}; + } + if (!missingTranslations[localeCode][packageInfo.key]) { + missingTranslations[localeCode][packageInfo.key] = { + path: localePath.replace(workspaceRoot, '').slice(1), // Remove leading slash + missingKeys: [], + }; + } + const lines = rawCode.split('\n'); + Object.entries(baseTranslations).forEach(([key]) => { + if (!existingTranslations[key]) { + const location = lines.findIndex((line) => line.trim().startsWith(`// ${key}:`)); + // Ignore when both the translation and the placeholder are missing + if (location >= 0) { + missingTranslations[localeCode][packageInfo.key].missingKeys.push({ + currentLineContent: lines[location].trim().slice(3), + lineIndex: location + 1, + }); + } } + }); + + if (!report) { + fse.writeFileSync(localePath, prettifiedCode); + // eslint-disable-next-line no-console + console.log(`Wrote ${localeCode} locale.`); } }); - - if (!report) { - fse.writeFileSync(localePath, prettifiedCode); - // eslint-disable-next-line no-console - console.log(`Wrote ${localeCode} locale.`); - } }); if (report) {