/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/build/src/utils/i18n-options.ts
293 строки
9 KB
igord
fix(@angular-devkit/build-angular): respect i18nDuplicateTranslation option when duplicates exist
20 май 2025, 18:43
20 май 2025, 18:43
d16e358
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import path from 'node:path'; import type { TranslationLoader } from './load-translations'; export interface LocaleDescription { files: { path: string; integrity?: string; format?: string; }[]; translation?: Record<string, unknown>; dataPath?: string; baseHref?: string; subPath: string; } export interface I18nOptions { inlineLocales: Set<string>; sourceLocale: string; locales: Record<string, LocaleDescription>; flatOutput?: boolean; readonly shouldInline: boolean; hasDefinedSourceLocale?: boolean; } function normalizeTranslationFileOption( option: unknown, locale: string, expectObjectInError: boolean, ): string[] { if (typeof option === 'string') { return [option]; } if (Array.isArray(option) && option.every((element) => typeof element === 'string')) { return option; } let errorMessage = `Project i18n locales translation field value for '${locale}' is malformed. `; if (expectObjectInError) { errorMessage += 'Expected a string, array of strings, or object.'; } else { errorMessage += 'Expected a string or array of strings.'; } throw new Error(errorMessage); } function ensureObject(value: unknown, name: string): asserts value is Record<string, unknown> { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new Error(`Project field '${name}' is malformed. Expected an object.`); } } function ensureString(value: unknown, name: string): asserts value is string { if (typeof value !== 'string') { throw new Error(`Project field '${name}' is malformed. Expected a string.`); } } function ensureValidSubPath(value: unknown, name: string): asserts value is string { ensureString(value, name); if (!/^[\w-]*$/.test(value)) { throw new Error( `Project field '${name}' is invalid. It can only contain letters, numbers, hyphens, and underscores.`, ); } } export function createI18nOptions( projectMetadata: { i18n?: unknown }, inline?: boolean | string[], logger?: { warn(message: string): void; }, ssrEnabled?: boolean, ): I18nOptions { const { i18n: metadata = {} } = projectMetadata; ensureObject(metadata, 'i18n'); const i18n: I18nOptions = { inlineLocales: new Set<string>(), // en-US is the default locale added to Angular applications (https://angular.dev/guide/i18n/format-data-locale) sourceLocale: 'en-US', locales: {}, get shouldInline() { return this.inlineLocales.size > 0; }, }; let rawSourceLocale: string | undefined; let rawSourceLocaleBaseHref: string | undefined; let rawsubPath: string | undefined; if (typeof metadata.sourceLocale === 'string') { rawSourceLocale = metadata.sourceLocale; } else if (metadata.sourceLocale !== undefined) { ensureObject(metadata.sourceLocale, 'i18n.sourceLocale'); if (metadata.sourceLocale.code !== undefined) { ensureString(metadata.sourceLocale.code, 'i18n.sourceLocale.code'); rawSourceLocale = metadata.sourceLocale.code; } if (metadata.sourceLocale.baseHref !== undefined) { ensureString(metadata.sourceLocale.baseHref, 'i18n.sourceLocale.baseHref'); if (ssrEnabled) { logger?.warn( `'baseHref' in 'i18n.sourceLocale' may lead to undefined behavior when used with SSR. ` + `Consider using 'subPath' instead.\n\n` + `Note: 'subPath' specifies the URL segment for the locale, serving as both the HTML base HREF ` + `and the output directory name.\nBy default, if not explicitly set, 'subPath' defaults to the locale code.`, ); } rawSourceLocaleBaseHref = metadata.sourceLocale.baseHref; } if (metadata.sourceLocale.subPath !== undefined) { ensureValidSubPath(metadata.sourceLocale.subPath, 'i18n.sourceLocale.subPath'); rawsubPath = metadata.sourceLocale.subPath; } if (rawsubPath !== undefined && rawSourceLocaleBaseHref !== undefined) { throw new Error( `'i18n.sourceLocale.subPath' and 'i18n.sourceLocale.baseHref' cannot be used together.`, ); } } if (rawSourceLocale !== undefined) { i18n.sourceLocale = rawSourceLocale; i18n.hasDefinedSourceLocale = true; } i18n.locales[i18n.sourceLocale] = { files: [], baseHref: rawSourceLocaleBaseHref, subPath: rawsubPath ?? i18n.sourceLocale, }; if (metadata.locales !== undefined) { ensureObject(metadata.locales, 'i18n locales'); for (const [locale, options] of Object.entries(metadata.locales)) { let translationFiles: string[] | undefined; let baseHref: string | undefined; let subPath: string | undefined; if (options && typeof options === 'object' && 'translation' in options) { translationFiles = normalizeTranslationFileOption(options.translation, locale, false); if ('baseHref' in options) { ensureString(options.baseHref, `i18n.locales.${locale}.baseHref`); if (ssrEnabled) { logger?.warn( `'baseHref' in 'i18n.locales.${locale}' may lead to undefined behavior when used with SSR. ` + `Consider using 'subPath' instead.\n\n` + `Note: 'subPath' specifies the URL segment for the locale, serving as both the HTML base HREF ` + `and the output directory name.\nBy default, if not explicitly set, 'subPath' defaults to the locale code.`, ); } baseHref = options.baseHref; } if ('subPath' in options) { ensureValidSubPath(options.subPath, `i18n.locales.${locale}.subPath`); subPath = options.subPath; } if (subPath !== undefined && baseHref !== undefined) { throw new Error( `'i18n.locales.${locale}.subPath' and 'i18n.locales.${locale}.baseHref' cannot be used together.`, ); } } else { translationFiles = normalizeTranslationFileOption(options, locale, true); } if (locale === i18n.sourceLocale) { throw new Error( `An i18n locale ('${locale}') cannot both be a source locale and provide a translation.`, ); } i18n.locales[locale] = { files: translationFiles.map((file) => ({ path: file })), baseHref, subPath: subPath ?? locale, }; } } if (inline === true) { i18n.inlineLocales.add(i18n.sourceLocale); Object.keys(i18n.locales).forEach((locale) => i18n.inlineLocales.add(locale)); } else if (inline) { for (const locale of inline) { if (!i18n.locales[locale] && i18n.sourceLocale !== locale) { throw new Error(`Requested locale '${locale}' is not defined for the project.`); } i18n.inlineLocales.add(locale); } } // Check that subPaths are unique only the locales that we are inlining. const localesData = Object.entries(i18n.locales).filter(([locale]) => i18n.inlineLocales.has(locale), ); for (let i = 0; i < localesData.length; i++) { const [localeA, { subPath: subPathA }] = localesData[i]; for (let j = i + 1; j < localesData.length; j++) { const [localeB, { subPath: subPathB }] = localesData[j]; if (subPathA === subPathB) { throw new Error( `Invalid i18n configuration: Locales '${localeA}' and '${localeB}' cannot have the same subPath: '${subPathB}'.`, ); } } } return i18n; } export function loadTranslations( locale: string, desc: LocaleDescription, workspaceRoot: string, loader: TranslationLoader, logger: { warn: (message: string) => void; error: (message: string) => void }, usedFormats?: Set<string>, duplicateTranslation?: 'ignore' | 'error' | 'warning', ) { let translations: Record<string, unknown> | undefined = undefined; for (const file of desc.files) { const loadResult = loader(path.join(workspaceRoot, file.path)); for (const diagnostics of loadResult.diagnostics.messages) { if (diagnostics.type === 'error') { logger.error(`Error parsing translation file '${file.path}': ${diagnostics.message}`); } else { logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`); } } if (loadResult.locale !== undefined && loadResult.locale !== locale) { logger.warn( `WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`, ); } usedFormats?.add(loadResult.format); file.format = loadResult.format; file.integrity = loadResult.integrity; if (translations) { // Merge translations for (const [id, message] of Object.entries(loadResult.translations)) { if (translations[id] !== undefined) { const duplicateTranslationMessage = `[${file.path}]: Duplicate translations for message '${id}' when merging.`; switch (duplicateTranslation) { case 'ignore': break; case 'error': logger.error(`ERROR ${duplicateTranslationMessage}`); break; case 'warning': default: logger.warn(`WARNING ${duplicateTranslationMessage}`); break; } } translations[id] = message; } } else { // First or only translation file translations = loadResult.translations; } } desc.translation = translations; }