diff --git a/.vscode/settings.json b/.vscode/settings.json index 6fd759eb7..bb54bcf11 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,6 +41,7 @@ "mochaExplorer.configFile": ".config/mocha.test-explorer.json", "cSpell.words": [ "cname", + "Combinatorially", "deserializers", "githubprivate", "linkcode", diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b59d6069..f96a3cfab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Beta + +## Breaking Changes + +- Removed deprecated `navigation.fullTree` option. + # Unreleased ## Features diff --git a/src/lib/application.ts b/src/lib/application.ts index 5bb3b1ea9..6d6103e58 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -42,7 +42,10 @@ import { findTsConfigFile } from "./utils/tsconfig"; import { deriveRootDir, glob, readFile } from "./utils/fs"; import { resetReflectionID } from "./models/reflections/abstract"; import { addInferredDeclarationMapPaths } from "./models/reflections/ReflectionSymbolId"; -import { Internationalization } from "./internationalization/internationalization"; +import { + Internationalization, + TranslatedString, +} from "./internationalization/internationalization"; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageInfo = require("../../package.json") as { @@ -175,6 +178,7 @@ export class Application extends ChildableComponent< this.converter = this.addComponent("converter", Converter); this.renderer = this.addComponent("renderer", Renderer); + this.logger.i18n = this.i18n; } /** @@ -235,19 +239,20 @@ export class Application extends ChildableComponent< } this.trigger(ApplicationEvents.BOOTSTRAP_END, this); + // GERRIT: Add locales to i18n here. if (!this.internationalization.hasTranslations(this.lang)) { // Not internationalized as by definition we don't know what to include here. this.logger.warn( - `Options specified "${this.lang}" as the language to use, but TypeDoc does not support it.`, + `Options specified "${this.lang}" as the language to use, but TypeDoc does not support it.` as TranslatedString, ); this.logger.info( - "The supported languages are:\n\t" + + ("The supported languages are:\n\t" + this.internationalization .getSupportedLanguages() - .join("\n\t"), + .join("\n\t")) as TranslatedString, ); this.logger.info( - "You can define/override local locales with the `locales` option, or contribute them to TypeDoc!", + "You can define/override local locales with the `locales` option, or contribute them to TypeDoc!" as TranslatedString, ); } } @@ -259,7 +264,7 @@ export class Application extends ChildableComponent< } catch (error) { ok(error instanceof Error); if (reportErrors) { - this.logger.error(error.message); + this.logger.error(error.message as TranslatedString); // GERRIT review } } } @@ -433,7 +438,7 @@ export class Application extends ChildableComponent< ts.flattenDiagnosticMessageText( status.messageText, newLine, - ), + ) as TranslatedString, ); }, ); diff --git a/src/lib/converter/comments/discovery.ts b/src/lib/converter/comments/discovery.ts index 86ed6f234..444f1f2b8 100644 --- a/src/lib/converter/comments/discovery.ts +++ b/src/lib/converter/comments/discovery.ts @@ -201,7 +201,9 @@ export function discoverComment( return discovered[0]; default: { logger.warn( - `${symbol.name} has multiple declarations with a comment. An arbitrary comment will be used.`, + logger.i18n.symbol_0_has_multiple_declarations_with_comment( + symbol.name, + ), ); const locations = discovered.map(({ file, ranges: [{ pos }] }) => { const path = nicePath(file.fileName); @@ -210,9 +212,10 @@ export function discoverComment( return `${path}:${line}`; }); logger.info( - `The comments for ${ - symbol.name - } are declared at:\n\t${locations.join("\n\t")}`, + logger.i18n.comments_for_0_are_declared_at_1( + symbol.name, + locations.join("\n\t"), + ), ); return discovered[0]; } diff --git a/src/lib/converter/comments/index.ts b/src/lib/converter/comments/index.ts index c162751af..9e9fa5d8f 100644 --- a/src/lib/converter/comments/index.ts +++ b/src/lib/converter/comments/index.ts @@ -279,7 +279,7 @@ export function getJsDocComment( // we'd have to search for any @template with a name starting with the first type parameter's name // which feels horribly hacky. logger.warn( - `TypeDoc does not support multiple type parameters defined in a single @template tag with a comment.`, + logger.i18n.multiple_type_parameters_on_template_tag_unsupported(), declaration, ); return; @@ -301,7 +301,7 @@ export function getJsDocComment( if (!tag) { logger.error( - `Failed to find JSDoc tag for ${name} after parsing comment, please file a bug report.`, + logger.i18n.failed_to_find_jsdoc_tag_for_name_0(name), declaration, ); } else { diff --git a/src/lib/converter/comments/parser.ts b/src/lib/converter/comments/parser.ts index df89e06ab..f20a868a6 100644 --- a/src/lib/converter/comments/parser.ts +++ b/src/lib/converter/comments/parser.ts @@ -11,6 +11,10 @@ import type { MinimalSourceFile } from "../../utils/minimalSourceFile"; import { nicePath } from "../../utils/paths"; import { Token, TokenSyntaxKind } from "./lexer"; import { extractTagName } from "./tagName"; +import type { + TranslatedString, + TranslationProxy, +} from "../../internationalization/internationalization"; interface LookaheadGenerator { done(): boolean; @@ -70,24 +74,35 @@ export function parseComment( const tok = lexer.done() || lexer.peek(); const comment = new Comment(); - comment.summary = blockContent(comment, lexer, config, warningImpl); + comment.summary = blockContent( + comment, + lexer, + config, + logger.i18n, + warningImpl, + ); while (!lexer.done()) { - comment.blockTags.push(blockTag(comment, lexer, config, warningImpl)); + comment.blockTags.push( + blockTag(comment, lexer, config, logger.i18n, warningImpl), + ); } - postProcessComment(comment, (message) => { - ok(typeof tok === "object"); - logger.warn( - `${message} in comment at ${nicePath(file.fileName)}:${ - file.getLineAndCharacterOfPosition(tok.pos).line + 1 + const tok2 = tok as Token; + + postProcessComment( + comment, + logger.i18n, + () => + `${nicePath(file.fileName)}:${ + file.getLineAndCharacterOfPosition(tok2.pos).line + 1 }`, - ); - }); + (message) => logger.warn(message), + ); return comment; - function warningImpl(message: string, token: Token) { + function warningImpl(message: TranslatedString, token: Token) { logger.warn(message, token.pos, file); } } @@ -111,7 +126,12 @@ function makeCodeBlock(text: string) { * Loop over comment, produce lint warnings, and set tag names for tags * which have them. */ -function postProcessComment(comment: Comment, warning: (msg: string) => void) { +function postProcessComment( + comment: Comment, + i18n: TranslationProxy, + getPosition: () => string, + warning: (msg: TranslatedString) => void, +) { for (const tag of comment.blockTags) { if (HAS_USER_IDENTIFIER.includes(tag.tag) && tag.content.length) { const first = tag.content[0]; @@ -134,7 +154,9 @@ function postProcessComment(comment: Comment, warning: (msg: string) => void) { ) ) { warning( - "An inline @inheritDoc tag should not appear within a block tag as it will not be processed", + i18n.inline_inheritdoc_should_not_appear_in_block_tag_in_comment_at_0( + getPosition(), + ), ); } } @@ -142,7 +164,9 @@ function postProcessComment(comment: Comment, warning: (msg: string) => void) { const remarks = comment.blockTags.filter((tag) => tag.tag === "@remarks"); if (remarks.length > 1) { warning( - "At most one @remarks tag is expected in a comment, ignoring all but the first", + i18n.at_most_one_remarks_tag_expected_in_comment_at_0( + getPosition(), + ), ); removeIf(comment.blockTags, (tag) => remarks.indexOf(tag) > 0); } @@ -150,7 +174,9 @@ function postProcessComment(comment: Comment, warning: (msg: string) => void) { const returns = comment.blockTags.filter((tag) => tag.tag === "@returns"); if (remarks.length > 1) { warning( - "At most one @returns tag is expected in a comment, ignoring all but the first", + i18n.at_most_one_returns_tag_expected_in_comment_at_0( + getPosition(), + ), ); removeIf(comment.blockTags, (tag) => returns.indexOf(tag) > 0); } @@ -164,7 +190,9 @@ function postProcessComment(comment: Comment, warning: (msg: string) => void) { if (inlineInheritDoc.length + inheritDoc.length > 1) { warning( - "At most one @inheritDoc tag is expected in a comment, ignoring all but the first", + i18n.at_most_one_inheritdoc_tag_expected_in_comment_at_0( + getPosition(), + ), ); const allInheritTags = [...inlineInheritDoc, ...inheritDoc]; removeIf(comment.summary, (part) => allInheritTags.indexOf(part) > 0); @@ -178,13 +206,17 @@ function postProcessComment(comment: Comment, warning: (msg: string) => void) { ) ) { warning( - "Content in the summary section will be overwritten by the @inheritDoc tag", + i18n.content_in_summary_overwritten_by_inheritdoc_in_comment_at_0( + getPosition(), + ), ); } if ((inlineInheritDoc.length || inheritDoc.length) && remarks.length) { warning( - "Content in the @remarks block will be overwritten by the @inheritDoc tag", + i18n.content_in_remarks_block_overwritten_by_inheritdoc_in_comment_at_0( + getPosition(), + ), ); } } @@ -195,7 +227,8 @@ function blockTag( comment: Comment, lexer: LookaheadGenerator, config: CommentParserConfig, - warning: (msg: string, token: Token) => void, + i18n: TranslationProxy, + warning: (msg: TranslatedString, token: Token) => void, ): CommentTag { const blockTag = lexer.take(); ok( @@ -207,14 +240,14 @@ function blockTag( let content: CommentDisplayPart[]; if (tagName === "@example") { - return exampleBlock(comment, lexer, config, warning); + return exampleBlock(comment, lexer, config, i18n, warning); } else if ( ["@default", "@defaultValue"].includes(tagName) && config.jsDocCompatibility.defaultTag ) { - content = defaultBlockContent(comment, lexer, config, warning); + content = defaultBlockContent(comment, lexer, config, i18n, warning); } else { - content = blockContent(comment, lexer, config, warning); + content = blockContent(comment, lexer, config, i18n, warning); } return new CommentTag(tagName as `@${string}`, content); @@ -228,15 +261,16 @@ function defaultBlockContent( comment: Comment, lexer: LookaheadGenerator, config: CommentParserConfig, - warning: (msg: string, token: Token) => void, + i18n: TranslationProxy, + warning: (msg: TranslatedString, token: Token) => void, ): CommentDisplayPart[] { lexer.mark(); - const content = blockContent(comment, lexer, config, () => {}); + const content = blockContent(comment, lexer, config, i18n, () => {}); const end = lexer.done() || lexer.peek(); lexer.release(); if (content.some((part) => part.kind === "code")) { - return blockContent(comment, lexer, config, warning); + return blockContent(comment, lexer, config, i18n, warning); } const tokens: Token[] = []; @@ -267,10 +301,11 @@ function exampleBlock( comment: Comment, lexer: LookaheadGenerator, config: CommentParserConfig, - warning: (msg: string, token: Token) => void, + i18n: TranslationProxy, + warning: (msg: TranslatedString, token: Token) => void, ): CommentTag { lexer.mark(); - const content = blockContent(comment, lexer, config, () => {}); + const content = blockContent(comment, lexer, config, i18n, () => {}); const end = lexer.done() || lexer.peek(); lexer.release(); @@ -307,11 +342,7 @@ function exampleBlock( case TokenSyntaxKind.CloseBrace: case TokenSyntaxKind.OpenBrace: if (!warnedAboutRichNameContent) { - warning( - "The first line of an example tag will be taken literally as" + - " the example name, and should only contain text.", - lexer.peek(), - ); + warning(i18n.example_tag_literal_name(), lexer.peek()); warnedAboutRichNameContent = true; } exampleName += lexer.take().text; @@ -321,7 +352,7 @@ function exampleBlock( } } - const content = blockContent(comment, lexer, config, warning); + const content = blockContent(comment, lexer, config, i18n, warning); const tag = new CommentTag("@example", content); if (exampleName.trim()) { tag.name = exampleName.trim(); @@ -364,7 +395,8 @@ function blockContent( comment: Comment, lexer: LookaheadGenerator, config: CommentParserConfig, - warning: (msg: string, token: Token) => void, + i18n: TranslationProxy, + warning: (msg: TranslatedString, token: Token) => void, ): CommentDisplayPart[] { const content: CommentDisplayPart[] = []; let atNewLine = true; @@ -387,7 +419,7 @@ function blockContent( if (next.text === "@inheritdoc") { if (!config.jsDocCompatibility.inheritDocTag) { warning( - "The @inheritDoc tag should be properly capitalized", + i18n.inheritdoc_tag_properly_capitalized(), next, ); } @@ -400,7 +432,7 @@ function blockContent( // Treat unknown tag as a modifier, but warn about it. comment.modifierTags.add(next.text as `@${string}`); warning( - `Treating unrecognized tag "${next.text}" as a modifier tag`, + i18n.treating_unrecognized_tag_0_as_modifier(next.text), next, ); break; @@ -417,13 +449,13 @@ function blockContent( case TokenSyntaxKind.CloseBrace: // Unmatched closing brace, generate a warning, and treat it as text. if (!config.jsDocCompatibility.ignoreUnescapedBraces) { - warning(`Unmatched closing brace`, next); + warning(i18n.unmatched_closing_brace(), next); } content.push({ kind: "text", text: next.text }); break; case TokenSyntaxKind.OpenBrace: - inlineTag(lexer, content, config, warning); + inlineTag(lexer, content, config, i18n, warning); consume = false; break; @@ -469,7 +501,8 @@ function inlineTag( lexer: LookaheadGenerator, block: CommentDisplayPart[], config: CommentParserConfig, - warning: (msg: string, token: Token) => void, + i18n: TranslationProxy, + warning: (msg: TranslatedString, token: Token) => void, ) { const openBrace = lexer.take(); @@ -481,10 +514,7 @@ function inlineTag( ![TokenSyntaxKind.Text, TokenSyntaxKind.Tag].includes(lexer.peek().kind) ) { if (!config.jsDocCompatibility.ignoreUnescapedBraces) { - warning( - "Encountered an unescaped open brace without an inline tag", - openBrace, - ); + warning(i18n.unescaped_open_brace_without_inline_tag(), openBrace); } block.push({ kind: "text", text: openBrace.text }); return; @@ -499,10 +529,7 @@ function inlineTag( lexer.peek().kind != TokenSyntaxKind.Tag)) ) { if (!config.jsDocCompatibility.ignoreUnescapedBraces) { - warning( - "Encountered an unescaped open brace without an inline tag", - openBrace, - ); + warning(i18n.unescaped_open_brace_without_inline_tag(), openBrace); } block.push({ kind: "text", text: openBrace.text + tagName.text }); return; @@ -513,7 +540,7 @@ function inlineTag( } if (!config.inlineTags.has(tagName.text)) { - warning(`Encountered an unknown inline tag "${tagName.text}"`, tagName); + warning(i18n.unknown_inline_tag_0(tagName.text), tagName); } const content: string[] = []; @@ -523,17 +550,14 @@ function inlineTag( while (!lexer.done() && lexer.peek().kind !== TokenSyntaxKind.CloseBrace) { const token = lexer.take(); if (token.kind === TokenSyntaxKind.OpenBrace) { - warning( - "Encountered an open brace within an inline tag, this is likely a mistake", - token, - ); + warning(i18n.open_brace_within_inline_tag(), token); } content.push(token.kind === TokenSyntaxKind.NewLine ? " " : token.text); } if (lexer.done()) { - warning("Inline tag is not closed", openBrace); + warning(i18n.inline_tag_not_closed(), openBrace); } else { lexer.take(); // Close brace } diff --git a/src/lib/converter/context.ts b/src/lib/converter/context.ts index 63beb4be4..bb993f254 100644 --- a/src/lib/converter/context.ts +++ b/src/lib/converter/context.ts @@ -21,6 +21,7 @@ import { getSignatureComment, } from "./comments"; import { getHumanName } from "../utils/tsutils"; +import type { TranslationProxy } from "../internationalization/internationalization"; /** * The context describes the current state the converter is in. @@ -38,6 +39,13 @@ export class Context { return this.program.getTypeChecker(); } + /** + * Translation interface for log messages. + */ + get i18n(): TranslationProxy { + return this.converter.application.i18n; + } + /** * The program currently being converted. * Accessing this property will throw if a source file is not currently being converted. diff --git a/src/lib/converter/converter.ts b/src/lib/converter/converter.ts index 445f3880c..cc4af5158 100644 --- a/src/lib/converter/converter.ts +++ b/src/lib/converter/converter.ts @@ -414,9 +414,9 @@ export class Converter extends ChildableComponent< ...comment.modifierTags, ]; context.logger.warn( - `Block and modifier tags will be ignored within the readme:\n\t${ignored.join( - "\n\t", - )}`, + this.application.i18n.block_and_modifier_tags_ignored_within_readme_0( + ignored.join("\n\t"), + ), ); } diff --git a/src/lib/converter/plugins/CategoryPlugin.ts b/src/lib/converter/plugins/CategoryPlugin.ts index d91de7dc0..d015f2771 100644 --- a/src/lib/converter/plugins/CategoryPlugin.ts +++ b/src/lib/converter/plugins/CategoryPlugin.ts @@ -90,10 +90,9 @@ export class CategoryPlugin extends ConverterComponent { if (unusedBoosts.size) { context.logger.warn( - `Not all categories specified in searchCategoryBoosts were used in the documentation.` + - ` The unused categories were:\n\t${Array.from( - unusedBoosts, - ).join("\n\t")}`, + context.i18n.not_all_search_category_boosts_used_0( + Array.from(unusedBoosts).join("\n\t"), + ), ); } } diff --git a/src/lib/converter/plugins/CommentPlugin.ts b/src/lib/converter/plugins/CommentPlugin.ts index 997b0024f..032e58435 100644 --- a/src/lib/converter/plugins/CommentPlugin.ts +++ b/src/lib/converter/plugins/CommentPlugin.ts @@ -348,10 +348,10 @@ export class CommentPlugin extends ConverterComponent { !/[A-Z_][A-Z0-9_]/.test(reflection.comment.label) ) { context.logger.warn( - `The label "${ - reflection.comment.label - }" for ${reflection.getFriendlyFullName()} cannot be referenced with a declaration reference. ` + - `Labels may only contain A-Z, 0-9, and _, and may not start with a number.`, + context.i18n.label_0_for_1_cannot_be_referenced( + reflection.comment.label, + reflection.getFriendlyFullName(), + ), ); } @@ -618,9 +618,10 @@ export class CommentPlugin extends ConverterComponent { if (signatureHadOwnComment && paramTags.length) { for (const tag of paramTags) { this.application.logger.warn( - `The signature ${signature.getFriendlyFullName()} has an @param with name "${ - tag.name - }", which was not used.`, + this.application.i18n.signature_0_has_unused_param_with_name_1( + signature.getFriendlyFullName(), + tag.name ?? "(missing)", + ), ); } } diff --git a/src/lib/converter/plugins/GroupPlugin.ts b/src/lib/converter/plugins/GroupPlugin.ts index f92cefd32..734a1da08 100644 --- a/src/lib/converter/plugins/GroupPlugin.ts +++ b/src/lib/converter/plugins/GroupPlugin.ts @@ -79,10 +79,9 @@ export class GroupPlugin extends ConverterComponent { this.application.options.isSet("searchGroupBoosts") ) { context.logger.warn( - `Not all groups specified in searchGroupBoosts were used in the documentation.` + - ` The unused groups were:\n\t${Array.from( - unusedBoosts, - ).join("\n\t")}`, + context.i18n.not_all_search_group_boosts_used_0( + Array.from(unusedBoosts).join("\n\t"), + ), ); } } diff --git a/src/lib/converter/plugins/ImplementsPlugin.ts b/src/lib/converter/plugins/ImplementsPlugin.ts index 84a37083f..eee111c93 100644 --- a/src/lib/converter/plugins/ImplementsPlugin.ts +++ b/src/lib/converter/plugins/ImplementsPlugin.ts @@ -14,6 +14,7 @@ import { Component, ConverterComponent } from "../components"; import type { Context } from "../context"; import { Converter } from "../converter"; import { getHumanName } from "../../utils"; +import type { TranslatedString } from "../../internationalization/internationalization"; /** * A plugin that detects interface implementations of functions and @@ -318,7 +319,7 @@ export class ImplementsPlugin extends ConverterComponent { } member "${ reflection.escapedName ?? reflection.name }" of "${reflection.parent - ?.name}" for inheritance analysis. Please report a bug.`, + ?.name}" for inheritance analysis. Please report a bug.` as TranslatedString, ); return; } diff --git a/src/lib/converter/plugins/InheritDocPlugin.ts b/src/lib/converter/plugins/InheritDocPlugin.ts index abb7bf606..04c1844a6 100644 --- a/src/lib/converter/plugins/InheritDocPlugin.ts +++ b/src/lib/converter/plugins/InheritDocPlugin.ts @@ -65,7 +65,9 @@ export class InheritDocPlugin extends ConverterComponent { const declRef = parseDeclarationReference(source, 0, source.length); if (!declRef || /\S/.test(source.substring(declRef[1]))) { this.application.logger.warn( - `Declaration reference in @inheritDoc for ${reflection.getFriendlyFullName()} was not fully parsed and may resolve incorrectly.`, + this.application.i18n.declaration_reference_in_inheritdoc_for_0_not_fully_parsed( + reflection.getFriendlyFullName(), + ), ); } let sourceRefl = @@ -99,7 +101,10 @@ export class InheritDocPlugin extends ConverterComponent { if (!sourceRefl) { if (this.validation.invalidLink) { this.application.logger.warn( - `Failed to find "${source}" to inherit the comment from in the comment for ${reflection.getFullName()}`, + this.application.i18n.failed_to_find_0_to_inherit_comment_from_in_1( + source, + reflection.getFriendlyFullName(), + ), ); } continue; @@ -134,7 +139,10 @@ export class InheritDocPlugin extends ConverterComponent { if (!source.comment) { this.application.logger.warn( - `${target.getFullName()} tried to copy a comment from ${source.getFullName()} with @inheritDoc, but the source has no associated comment.`, + this.application.i18n.reflection_0_tried_to_copy_comment_from_1_but_source_had_no_comment( + target.getFullName(), + source.getFullName(), + ), ); return; } @@ -196,9 +204,9 @@ export class InheritDocPlugin extends ConverterComponent { parts.push(orig.name); this.application.logger.warn( - `@inheritDoc specifies a circular inheritance chain: ${parts - .reverse() - .join(" -> ")}`, + this.application.i18n.inheritdoc_circular_inheritance_chain_0( + parts.reverse().join(" -> "), + ), ); }; diff --git a/src/lib/converter/plugins/PackagePlugin.ts b/src/lib/converter/plugins/PackagePlugin.ts index 2032a92b7..f3255ca17 100644 --- a/src/lib/converter/plugins/PackagePlugin.ts +++ b/src/lib/converter/plugins/PackagePlugin.ts @@ -108,9 +108,9 @@ export class PackagePlugin extends ConverterComponent { this.readmeFile = this.readme; } catch { this.application.logger.error( - `Provided README path, ${nicePath( - this.readme, - )} could not be read.`, + this.application.i18n.provided_readme_at_0_could_not_be_read( + nicePath(this.readme), + ), ); } } else { @@ -156,9 +156,9 @@ export class PackagePlugin extends ConverterComponent { ...comment.modifierTags, ]; this.application.logger.warn( - `Block and modifier tags will be ignored within the readme:\n\t${ignored.join( - "\n\t", - )}`, + this.application.i18n.block_and_modifier_tags_ignored_within_readme_0( + ignored.join("\n\t"), + ), ); } @@ -178,7 +178,7 @@ export class PackagePlugin extends ConverterComponent { } } else if (!project.name) { this.application.logger.warn( - 'The --name option was not specified, and no package.json was found. Defaulting project name to "Documentation".', + this.application.i18n.defaulting_project_name(), ); project.name = "Documentation"; } diff --git a/src/lib/converter/plugins/SourcePlugin.ts b/src/lib/converter/plugins/SourcePlugin.ts index 03df0d54e..50b5ab540 100644 --- a/src/lib/converter/plugins/SourcePlugin.ts +++ b/src/lib/converter/plugins/SourcePlugin.ts @@ -154,7 +154,7 @@ export class SourcePlugin extends ConverterComponent { if (this.disableGit && !this.sourceLinkTemplate) { this.application.logger.error( - `disableGit is set, but sourceLinkTemplate is not, so source links cannot be produced. Set a sourceLinkTemplate or disableSources to prevent source tracking.`, + context.i18n.disable_git_set_but_not_source_link_template(), ); return; } @@ -164,7 +164,7 @@ export class SourcePlugin extends ConverterComponent { !this.gitRevision ) { this.application.logger.warn( - `disableGit is set and sourceLinkTemplate contains {gitRevision}, which will be replaced with an empty string as no revision was provided.`, + context.i18n.disable_git_set_and_git_revision_used(), ); } diff --git a/src/lib/converter/symbols.ts b/src/lib/converter/symbols.ts index d94e586a1..eb27dd6ad 100644 --- a/src/lib/converter/symbols.ts +++ b/src/lib/converter/symbols.ts @@ -385,7 +385,7 @@ function convertTypeAliasAsInterface( if (type.getFlags() & ts.TypeFlags.Union) { context.logger.warn( - `Using @interface on a union type will discard properties not present on all branches of the union. TypeDoc's output may not accurately describe your source code.`, + context.i18n.converting_union_as_interface(), declaration, ); } @@ -1106,7 +1106,9 @@ function convertSymbolAsClass( if (!symbol.valueDeclaration) { context.logger.error( - `No value declaration found when converting ${symbol.name} as a class`, + context.i18n.converting_0_as_class_requires_value_declaration( + symbol.name, + ), symbol.declarations?.[0], ); return; @@ -1160,7 +1162,9 @@ function convertSymbolAsClass( } } else { context.logger.warn( - `${reflection.getFriendlyFullName()} is being converted as a class, but does not have any construct signatures`, + context.i18n.converting_0_as_class_without_construct_signatures( + reflection.getFriendlyFullName(), + ), symbol.valueDeclaration, ); } diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index 2c8f589a8..21222a819 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -40,6 +40,7 @@ import { import { convertSymbol } from "./symbols"; import { isObjectType } from "./utils/nodes"; import { removeUndefined } from "./utils/reflections"; +import type { TranslatedString } from "../internationalization/internationalization"; export interface TypeConverter< TNode extends ts.TypeNode = ts.TypeNode, @@ -1065,14 +1066,14 @@ function requestBugReport(context: Context, nodeOrType: ts.Node | ts.Type) { if ("kind" in nodeOrType) { const kindName = ts.SyntaxKind[nodeOrType.kind]; context.logger.warn( - `Failed to convert type node with kind: ${kindName} and text ${nodeOrType.getText()}. Please report a bug.`, + `Failed to convert type node with kind: ${kindName} and text ${nodeOrType.getText()}. Please report a bug.` as TranslatedString, nodeOrType, ); return new UnknownType(nodeOrType.getText()); } else { const typeString = context.checker.typeToString(nodeOrType); context.logger.warn( - `Failed to convert type: ${typeString} when converting ${context.scope.getFullName()}. Please report a bug.`, + `Failed to convert type: ${typeString} when converting ${context.scope.getFullName()}. Please report a bug.` as TranslatedString, ); return new UnknownType(typeString); } diff --git a/src/lib/converter/utils/repository.ts b/src/lib/converter/utils/repository.ts index 23fbbecc3..408268821 100644 --- a/src/lib/converter/utils/repository.ts +++ b/src/lib/converter/utils/repository.ts @@ -141,9 +141,7 @@ export class GitRepository implements Repository { remotesOut.stdout.split("\n"), ); } else { - logger.warn( - `The provided git remote "${gitRemote}" was not valid. Source links will be broken.`, - ); + logger.warn(logger.i18n.git_remote_0_not_valid(gitRemote)); } } diff --git a/src/lib/internationalization/internationalization.ts b/src/lib/internationalization/internationalization.ts index 6e3ff1872..1c0b3d468 100644 --- a/src/lib/internationalization/internationalization.ts +++ b/src/lib/internationalization/internationalization.ts @@ -13,7 +13,8 @@ import { join } from "path"; * TypeDoc includes a lot of literal strings. By convention, messages which are displayed * to the user at the INFO level or above should be present in this object to be available * for translation. Messages at the VERBOSE level need not be translated as they are primarily - * intended for debugging. + * intended for debugging. ERROR/WARNING deprecation messages related to TypeDoc's API, or + * requesting users submit a bug report need not be translated. * * Errors thrown by TypeDoc are generally *not* considered translatable as they are not * displayed to the user. An exception to this is errors thrown by the `validate` method @@ -44,13 +45,16 @@ import { join } from "path"; */ export interface TranslatableStrings extends BuiltinTranslatableStringArgs {} +declare const TranslatedString: unique symbol; +export type TranslatedString = string & { [TranslatedString]: true }; + /** * Dynamic proxy type built from {@link TranslatableStrings} */ export type TranslationProxy = { [K in keyof TranslatableStrings]: ( ...args: TranslatableStrings[K] - ) => string; + ) => TranslatedString; }; // If we're running in ts-node, then we need the TS source rather than @@ -82,8 +86,12 @@ export class Internationalization { }, ); - /** @internal */ - constructor(private application: Application) {} + /** + * If constructed without an application, will use the default language. + * Intended for use in unit tests only. + * @internal + */ + constructor(private application: Application | null) {} /** * Get the translation of the specified key, replacing placeholders @@ -92,14 +100,14 @@ export class Internationalization { translate( key: T, ...args: TranslatableStrings[T] - ): string { + ): TranslatedString { return ( - this.allTranslations.get(this.application.lang).get(key) ?? + this.allTranslations.get(this.application?.lang ?? "en").get(key) ?? translatable[key] ?? key ).replace(/\{(\d+)\}/g, (_, index) => { return args[+index] ?? "(no placeholder)"; - }); + }) as TranslatedString; } /** diff --git a/src/lib/internationalization/translatable.ts b/src/lib/internationalization/translatable.ts index e4b622601..82fcc2fda 100644 --- a/src/lib/internationalization/translatable.ts +++ b/src/lib/internationalization/translatable.ts @@ -18,6 +18,8 @@ export const translatable = { no_compiler_options_set: "No compiler options set. This likely means that TypeDoc did not find your tsconfig.json. Generated documentation will probably be empty.", + loaded_plugin_0: `Loaded plugin {0}`, + solution_not_supported_in_watch_mode: "The provided tsconfig file looks like a solution style tsconfig, which is not supported in watch mode.", strategy_not_supported_in_watch_mode: @@ -43,6 +45,126 @@ export const translatable = { entrypoint_did_not_match_files_0: "The entrypoint glob {0} did not match any files.", failed_to_parse_json_0: `Failed to parse file at {0} as json.`, + + block_and_modifier_tags_ignored_within_readme_0: `Block and modifier tags will be ignored within the readme:\n\t{0}`, + + converting_union_as_interface: `Using @interface on a union type will discard properties not present on all branches of the union. TypeDoc's output may not accurately describe your source code.`, + converting_0_as_class_requires_value_declaration: `Converting {0} as a class requires a declaration which represents a non-type value.`, + converting_0_as_class_without_construct_signatures: `{0} is being converted as a class, but does not have any construct signatures`, + + symbol_0_has_multiple_declarations_with_comment: `{0} has multiple declarations with a comment. An arbitrary comment will be used.`, + comments_for_0_are_declared_at_1: `The comments for {0} are declared at:\n\t{1}`, + + // comments/parser.ts + multiple_type_parameters_on_template_tag_unsupported: `TypeDoc does not support multiple type parameters defined in a single @template tag with a comment.`, + failed_to_find_jsdoc_tag_for_name_0: `Failed to find JSDoc tag for {0} after parsing comment, please file a bug report.`, + + inline_inheritdoc_should_not_appear_in_block_tag_in_comment_at_0: + "An inline @inheritDoc tag should not appear within a block tag as it will not be processed in comment at {0}", + at_most_one_remarks_tag_expected_in_comment_at_0: + "At most one @remarks tag is expected in a comment, ignoring all but the first in comment at {0}", + at_most_one_returns_tag_expected_in_comment_at_0: + "At most one @returns tag is expected in a comment, ignoring all but the first in comment at {0}", + at_most_one_inheritdoc_tag_expected_in_comment_at_0: + "At most one @inheritDoc tag is expected in a comment, ignoring all but the first in comment at {0}", + content_in_summary_overwritten_by_inheritdoc_in_comment_at_0: + "Content in the summary section will be overwritten by the @inheritDoc tag in comment at {0}", + content_in_remarks_block_overwritten_by_inheritdoc_in_comment_at_0: + "Content in the @remarks block will be overwritten by the @inheritDoc tag in comment at {0}", + example_tag_literal_name: + "The first line of an example tag will be taken literally as the example name, and should only contain text.", + inheritdoc_tag_properly_capitalized: + "The @inheritDoc tag should be properly capitalized.", + treating_unrecognized_tag_0_as_modifier: `Treating unrecognized tag {0} as a modifier tag.`, + unmatched_closing_brace: `Unmatched closing brace.`, + unescaped_open_brace_without_inline_tag: `Encountered an unescaped open brace without an inline tag.`, + unknown_inline_tag_0: `Encountered an unknown inline tag {0}.`, + open_brace_within_inline_tag: `Encountered an open brace within an inline tag, this is likely a mistake.`, + inline_tag_not_closed: `Inline tag is not closed.`, + + // validation + failed_to_resolve_link_to_0_in_comment_for_1: `Failed to resolve link to "{0}" in comment for {1}`, + type_0_defined_in_1_is_referenced_by_2_but_not_included_in_docs: `{0}, defined in {1}, is referenced by {2} but not included in the documentation.`, + reflection_0_kind_1_defined_in_2_does_not_have_any_documentation: `{0} ({1}), defined in {2}, does not have any documentation.`, + invalid_intentionally_not_exported_symbols_0: + "The following symbols were marked as intentionally not exported, but were either not referenced in the documentation, or were exported:\n\t{0}", + + // conversion plugins + not_all_search_category_boosts_used_0: `Not all categories specified in searchCategoryBoosts were used in the documentation. The unused categories were:\n\t{0}`, + not_all_search_group_boosts_used_0: `Not all groups specified in searchGroupBoosts were used in the documentation. The unused groups were:\n\t{0}`, + label_0_for_1_cannot_be_referenced: `The label "{0}" for {1} cannot be referenced with a declaration reference. Labels may only contain A-Z, 0-9, and _, and may not start with a number.`, + signature_0_has_unused_param_with_name_1: `The signature {0} has an @param with name "{1}", which was not used.`, + declaration_reference_in_inheritdoc_for_0_not_fully_parsed: `Declaration reference in @inheritDoc for {0} was not fully parsed and may resolve incorrectly.`, + failed_to_find_0_to_inherit_comment_from_in_1: `Failed to find "{0}" to inherit the comment from in the comment for {1}`, + reflection_0_tried_to_copy_comment_from_1_but_source_had_no_comment: `{0} tried to copy a comment from {1} with @inheritDoc, but the source has no associated comment.`, + inheritdoc_circular_inheritance_chain_0: `@inheritDoc specifies a circular inheritance chain: {0}`, + provided_readme_at_0_could_not_be_read: `Provided README path, {0} could not be read.`, + defaulting_project_name: + 'The --name option was not specified, and no package.json was found. Defaulting project name to "Documentation".', + disable_git_set_but_not_source_link_template: `disableGit is set, but sourceLinkTemplate is not, so source links cannot be produced. Set a sourceLinkTemplate or disableSources to prevent source tracking.`, + disable_git_set_and_git_revision_used: `disableGit is set and sourceLinkTemplate contains {gitRevision}, which will be replaced with an empty string as no revision was provided.`, + provided_git_remote_0_was_invalid: `The provided git remote "{0}" was not valid. Source links will be broken.`, + git_remote_0_not_valid: `The provided git remote "{0}" was not valid. Source links will be broken.`, + + // output plugins + custom_css_file_0_does_not_exist: `Custom CSS file at {0} does not exist.`, + unsupported_highlight_language_0_not_highlighted_in_comment_for_1: `Unsupported highlight language {0} will not be highlighted in comment for {1}.`, + could_not_find_file_to_include_0: `Could not find file to include: {0}`, + could_not_find_media_file_0: `Could not find media file: {0}`, + could_not_find_includes_directory: + "Could not find provided includes directory: {0}", + could_not_find_media_directory: + "Could not find provided media directory: {0}", + + // renderer + could_not_write_0: `Could not write {0}`, + could_not_empty_output_directory_0: `Could not empty the output directory {0}`, + could_not_create_output_directory_0: `Could not create the output directory {0}`, + theme_0_is_not_defined_available_are_1: `The theme '{0}' is not defined. The available themes are: {1}`, + + // entry points + no_entry_points_provided: + "No entry points were provided, this is likely a misconfiguration.", + unable_to_find_any_entry_points: + "Unable to find any entry points. See previous warnings.", + watch_does_not_support_packages_mode: + "Watch mode does not support 'packages' style entry points.", + watch_does_not_support_merge_mode: + "Watch mode does not support 'merge' style entry points.", + entry_point_0_not_in_program: `The entry point {0} is not referenced by the 'files' or 'include' option in your tsconfig.`, + use_expand_or_glob_for_files_in_dir: `If you wanted to include files inside this directory, set --entryPointStrategy to expand or specify a glob.`, + entry_point_0_did_not_match_any_files: `The entry point glob {0} did not match any files.`, + entry_point_0_did_not_match_any_files_after_exclude: `The entry point glob {0} did not match any files after applying exclude patterns.`, + entry_point_0_did_not_exist: `Provided entry point {0} does not exist.`, + entry_point_0_did_not_match_any_packages: `The entry point glob {0} did not match any directories containing package.json.`, + file_0_not_an_object: `The file {0} is not an object.`, + + // deserialization + serialized_project_referenced_0_not_part_of_project: `Serialized project referenced reflection {0}, which was not a part of the project.`, + + // options + circular_reference_extends_0: `Circular reference encountered for "extends" field of {0}`, + failed_resolve_0_to_file_in_1: `Failed to resolve {0} to a file in {1}`, + + option_0_can_only_be_specified_by_config_file: `The '{0}' option can only be specified via a config file.`, + option_0_expected_a_value_but_none_provided: `--{0} expected a value, but none was given as an argument`, + unknown_option_0_may_have_meant_1: `Unknown option: {0}, you may have meant:\n\t{1}`, + + typedoc_key_in_0_ignored: `The 'typedoc' key in {0} was used by the legacy-packages entryPointStrategy and will be ignored.`, + typedoc_options_must_be_object_in_0: `Failed to parse the "typedocOptions" field in {0}, ensure it exists and contains an object.`, + tsconfig_file_0_does_not_exist: `The tsconfig file {0} does not exist`, + tsconfig_file_specifies_options_file: `"typedocOptions" in tsconfig file specifies an option file to read but the option file has already been read. This is likely a misconfiguration.`, + tsconfig_file_specifies_tsconfig_file: `"typedocOptions" in tsconfig file may not specify a tsconfig file to read.`, + tags_0_defined_in_typedoc_json_overwritten_by_tsdoc_json: `The {0} defined in typedoc.json will be overwritten by configuration in tsdoc.json.`, + failed_read_tsdoc_json_0: `Failed to read tsdoc.json file at {0}.`, + invalid_tsdoc_json_0: `The file {0} is not a valid tsdoc.json file.`, + + options_file_0_does_not_exist: `The options file {0} does not exist.`, + failed_read_options_file_0: `Failed to parse {0}, ensure it exists and exports an object.`, + + // plugins + invalid_plugin_0_missing_load_function: `Invalid structure in plugin {0}, no load function found.`, + plugin_0_could_not_be_loaded: `The plugin {0} could not be loaded.`, } as const; export type BuiltinTranslatableStringArgs = { @@ -62,13 +184,25 @@ export type BuiltinTranslatableStringConstraints = { [K in keyof BuiltinTranslatableStringArgs]: TranslationConstraint[BuiltinTranslatableStringArgs[K]["length"]]; }; +type BuildConstraint< + T extends number, + Acc extends string = "", + U extends number = T, +> = [T] extends [never] + ? `${Acc}${string}` + : T extends T + ? BuildConstraint, `${Acc}${string}{${T}}`> + : never; + +// Combinatorially explosive, but shouldn't matter for us, since we only need a few iterations. type TranslationConstraint = [ string, - `${string}{0}${string}`, - `${string}{0}${string}{1}${string}` | `${string}{1}${string}{0}${string}`, + BuildConstraint<0>, + BuildConstraint<0 | 1>, + BuildConstraint<0 | 1 | 2>, ]; -// Compiler errors here which says a property is missing indicates that the key on translatable +// Compiler errors here which says a property is missing indicates that the value on translatable // is not a literal string. It should be so that TypeDoc's placeholder replacement detection // can validate that all placeholders have been specified. const _validateLiteralStrings: { @@ -77,3 +211,14 @@ const _validateLiteralStrings: { : never]: never; } = {}; _validateLiteralStrings; + +// Compiler errors here which says a property is missing indicates that the key on translatable +// contains a placeholder _0/_1, etc. but the value does not match the expected constraint. +const _validatePlaceholdersPresent: { + [K in keyof typeof translatable]: K extends `${string}_1${string}` + ? TranslationConstraint[2] + : K extends `${string}_0${string}` + ? TranslationConstraint[1] + : TranslationConstraint[0]; +} = translatable; +_validatePlaceholdersPresent; diff --git a/src/lib/models/comments/comment.ts b/src/lib/models/comments/comment.ts index abefe74dd..817422cf4 100644 --- a/src/lib/models/comments/comment.ts +++ b/src/lib/models/comments/comment.ts @@ -294,7 +294,9 @@ export class Comment { ); if (!part.target) { de.logger.warn( - `Serialized project contained a link to ${oldId} (${part.text}), which was not a part of the project.`, + de.application.i18n.serialized_project_referenced_0_not_part_of_project( + oldId.toString(), + ), ); } } diff --git a/src/lib/models/reflections/declaration.ts b/src/lib/models/reflections/declaration.ts index ea316449a..727b738ad 100644 --- a/src/lib/models/reflections/declaration.ts +++ b/src/lib/models/reflections/declaration.ts @@ -341,7 +341,9 @@ export class DeclarationReflection extends ContainerReflection { ); } else { de.logger.warn( - `Serialized project contained a reflection with id ${id} but it was not present in deserialized project.`, + de.application.i18n.serialized_project_referenced_0_not_part_of_project( + id.toString(), + ), ); } } diff --git a/src/lib/models/reflections/project.ts b/src/lib/models/reflections/project.ts index 0485141e9..92eeb2377 100644 --- a/src/lib/models/reflections/project.ts +++ b/src/lib/models/reflections/project.ts @@ -373,7 +373,9 @@ export class ProjectReflection extends ContainerReflection { this.registerSymbolId(refl, new ReflectionSymbolId(sid)); } else { de.logger.warn( - `Serialized project contained a reflection with id ${id} but it was not present in deserialized project.`, + de.application.i18n.serialized_project_referenced_0_not_part_of_project( + id.toString(), + ), ); } } diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index be49d3a91..e42960702 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -1017,7 +1017,9 @@ export class ReferenceType extends Type { this._target = target.id; } else { de.logger.warn( - `Serialized project contained a reference to ${obj.target} (${this.qualifiedName}), which was not a part of the project.`, + de.application.i18n.serialized_project_referenced_0_not_part_of_project( + obj.target.toString(), + ), ); } }); diff --git a/src/lib/output/plugins/AssetsPlugin.ts b/src/lib/output/plugins/AssetsPlugin.ts index 98fa1a97b..531b55ea9 100644 --- a/src/lib/output/plugins/AssetsPlugin.ts +++ b/src/lib/output/plugins/AssetsPlugin.ts @@ -31,7 +31,9 @@ export class AssetsPlugin extends RendererComponent { copySync(this.customCss, join(dest, "custom.css")); } else { this.application.logger.error( - `Custom CSS file at ${this.customCss} does not exist.`, + this.application.i18n.custom_css_file_0_does_not_exist( + this.customCss, + ), ); event.preventDefault(); } diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index c3d4a7a5b..3f8b65d6d 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -323,7 +323,9 @@ export class Renderer extends ChildableComponent< try { writeFileSync(page.filename, page.contents); } catch (error) { - this.application.logger.error(`Could not write ${page.filename}`); + this.application.logger.error( + this.application.i18n.could_not_write_0(page.filename), + ); } } @@ -340,11 +342,10 @@ export class Renderer extends ChildableComponent< const ctor = this.themes.get(this.themeName); if (!ctor) { this.application.logger.error( - `The theme '${ - this.themeName - }' is not defined. The available themes are: ${[ - ...this.themes.keys(), - ].join(", ")}`, + this.application.i18n.theme_0_is_not_defined_available_are_1( + this.themeName, + [...this.themes.keys()].join(", "), + ), ); return false; } else { @@ -371,7 +372,9 @@ export class Renderer extends ChildableComponent< }); } catch (error) { this.application.logger.warn( - "Could not empty the output directory.", + this.application.i18n.could_not_empty_output_directory_0( + directory, + ), ); return false; } @@ -381,7 +384,9 @@ export class Renderer extends ChildableComponent< fs.mkdirSync(directory, { recursive: true }); } catch (error) { this.application.logger.error( - `Could not create output directory ${directory}.`, + this.application.i18n.could_not_create_output_directory_0( + directory, + ), ); return false; } @@ -396,7 +401,9 @@ export class Renderer extends ChildableComponent< fs.writeFileSync(path.join(directory, ".nojekyll"), text); } catch (error) { this.application.logger.warn( - "Could not create .nojekyll file.", + this.application.i18n.could_not_write_0( + path.join(directory, ".nojekyll"), + ), ); return false; } diff --git a/src/lib/output/themes/MarkedPlugin.tsx b/src/lib/output/themes/MarkedPlugin.tsx index 6d201537a..44b0d6b58 100644 --- a/src/lib/output/themes/MarkedPlugin.tsx +++ b/src/lib/output/themes/MarkedPlugin.tsx @@ -47,9 +47,6 @@ export class MarkedPlugin extends ContextAwareRendererComponent { */ private mediaPattern = /media:\/\/([^ ")\]}]+)/g; - private sources?: { fileName: string; line: number }[]; - private outputFileName?: string; - /** * Create a new MarkedPlugin instance. */ @@ -69,14 +66,12 @@ export class MarkedPlugin extends ContextAwareRendererComponent { lang = lang || "typescript"; lang = lang.toLowerCase(); if (!isSupportedLanguage(lang)) { - // Extra newline because of the progress bar - this.application.logger.warn(` -Unsupported highlight language "${lang}" will not be highlighted. Run typedoc --help for a list of supported languages. -target code block : -\t${text.split("\n").join("\n\t")} -source files :${this.sources?.map((source) => `\n\t${source.fileName}`).join()} -output file : -\t${this.outputFileName}`); + this.application.logger.warn( + this.application.i18n.unsupported_highlight_language_0_not_highlighted_in_comment_for_1( + lang, + this.page?.model.getFriendlyFullName() ?? "(unknown)", + ), + ); return text; } @@ -99,7 +94,7 @@ output file : this.owner.trigger(event); return event.parsedText; } else { - this.application.logger.warn("Could not find file to include: " + path); + this.application.logger.warn(this.application.i18n.could_not_find_file_to_include_0(path)); return ""; } }); @@ -112,7 +107,7 @@ output file : if (isFile(fileName)) { return this.getRelativeUrl("media") + "/" + path; } else { - this.application.logger.warn("Could not find media file: " + fileName); + this.application.logger.warn(this.application.i18n.could_not_find_media_file_0(fileName)); return match; } }); @@ -139,7 +134,9 @@ output file : if (fs.existsSync(this.includeSource) && fs.statSync(this.includeSource).isDirectory()) { this.includes = this.includeSource; } else { - this.application.logger.warn("Could not find provided includes directory: " + this.includeSource); + this.application.logger.warn( + this.application.i18n.could_not_find_includes_directory(this.includeSource), + ); } } @@ -149,7 +146,7 @@ output file : copySync(this.mediaSource, this.mediaDirectory); } else { this.mediaDirectory = undefined; - this.application.logger.warn("Could not find provided media directory: " + this.mediaSource); + this.application.logger.warn(this.application.i18n.could_not_find_media_directory(this.mediaSource)); } } } diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 301f8d6cf..1fe3c5b49 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -255,12 +255,6 @@ export class DefaultTheme extends Theme { const opts = this.application.options.getValue("navigation"); const leaves = this.application.options.getValue("navigationLeaves"); - if (opts.fullTree) { - this.application.logger.warn( - `The navigation.fullTree option no longer has any affect and will be removed in v0.26`, - ); - } - return getNavigationElements(project) || []; function toNavigation( diff --git a/src/lib/serialization/deserializer.ts b/src/lib/serialization/deserializer.ts index 45f8e6cc0..63d63cfcb 100644 --- a/src/lib/serialization/deserializer.ts +++ b/src/lib/serialization/deserializer.ts @@ -51,10 +51,10 @@ export class Deserializer { private deferred: Array<(project: ProjectReflection) => void> = []; private deserializers: DeserializerComponent[] = []; private activeReflection: Reflection[] = []; - constructor(private app: Application) {} + constructor(readonly application: Application) {} get logger(): Logger { - return this.app.logger; + return this.application.logger; } reflectionBuilders: { diff --git a/src/lib/utils/entry-point.ts b/src/lib/utils/entry-point.ts index 3d08865e9..32104807a 100644 --- a/src/lib/utils/entry-point.ts +++ b/src/lib/utils/entry-point.ts @@ -50,9 +50,7 @@ export function getEntryPoints( options: Options, ): DocumentationEntryPoint[] | undefined { if (!options.isSet("entryPoints")) { - logger.warn( - "No entry points were provided, this is likely a misconfiguration.", - ); + logger.warn(logger.i18n.no_entry_points_provided()); return []; } @@ -94,7 +92,7 @@ export function getEntryPoints( } if (result && result.length === 0) { - logger.error("Unable to find any entry points. See previous warnings."); + logger.error(logger.i18n.unable_to_find_any_entry_points()); return; } @@ -132,15 +130,11 @@ export function getWatchEntryPoints( break; case EntryPointStrategy.Packages: - logger.error( - "Watch mode does not support 'packages' style entry points.", - ); + logger.error(logger.i18n.watch_does_not_support_packages_mode()); break; case EntryPointStrategy.Merge: - logger.error( - "Watch mode does not support 'merge' style entry points.", - ); + logger.error(logger.i18n.watch_does_not_support_merge_mode()); break; default: @@ -148,7 +142,7 @@ export function getWatchEntryPoints( } if (result && result.length === 0) { - logger.error("Unable to find any entry points."); + logger.error(logger.i18n.unable_to_find_any_entry_points()); return; } @@ -187,6 +181,7 @@ function getEntryPointsForPaths( ): DocumentationEntryPoint[] { const baseDir = options.getValue("basePath") || deriveRootDir(inputFiles); const entryPoints: DocumentationEntryPoint[] = []; + let expandSuggestion = true; entryLoop: for (const fileOrDir of inputFiles.map(normalizePath)) { const toCheck = [fileOrDir]; @@ -217,14 +212,13 @@ function getEntryPointsForPaths( } } - const suggestion = isDir(fileOrDir) - ? " If you wanted to include files inside this directory, set --entryPointStrategy to expand or specify a glob." - : ""; logger.warn( - `The entry point ${nicePath( - fileOrDir, - )} is not referenced by the 'files' or 'include' option in your tsconfig.${suggestion}`, + logger.i18n.entry_point_0_not_in_program(nicePath(fileOrDir)), ); + if (expandSuggestion && isDir(fileOrDir)) { + expandSuggestion = false; + logger.info(logger.i18n.use_expand_or_glob_for_files_in_dir()); + } } return entryPoints; @@ -260,15 +254,15 @@ function expandGlobs(inputFiles: string[], exclude: string[], logger: Logger) { if (result.length === 0) { logger.warn( - `The entrypoint glob ${nicePath( - entry, - )} did not match any files.`, + logger.i18n.entry_point_0_did_not_match_any_files( + nicePath(entry), + ), ); } else if (filtered.length === 0) { logger.warn( - `The entrypoint glob ${nicePath( - entry, - )} did not match any files after applying exclude patterns.`, + logger.i18n.entry_point_0_did_not_match_any_files_after_exclude( + nicePath(entry), + ), ); } else { logger.verbose( @@ -382,9 +376,7 @@ function expandInputFiles( entryPoints.forEach((file) => { const resolved = resolve(file); if (!FS.existsSync(resolved)) { - logger.warn( - `Provided entry point ${file} does not exist and will not be included in the docs.`, - ); + logger.warn(logger.i18n.entry_point_0_did_not_exist(file)); return; } diff --git a/src/lib/utils/loggers.ts b/src/lib/utils/loggers.ts index 9a6cabfea..504562ba8 100644 --- a/src/lib/utils/loggers.ts +++ b/src/lib/utils/loggers.ts @@ -3,6 +3,11 @@ import { url } from "inspector"; import { resolve } from "path"; import { nicePath } from "./paths"; import type { MinimalSourceFile } from "./minimalSourceFile"; +import type { + TranslatedString, + TranslationProxy, +} from "../internationalization/internationalization"; +import type { IfInternal } from "."; const isDebugging = () => !!url(); @@ -39,6 +44,18 @@ const messagePrefixes = { [LogLevel.Verbose]: color("[debug]", "gray"), }; +const dummyTranslationProxy: TranslationProxy = new Proxy( + {} as TranslationProxy, + { + get: ({}, key) => { + return (...args: string[]) => + String(key).replace(/\{(\d+)\}/g, (_, index) => { + return args[+index] ?? "(no placeholder)"; + }); + }, + }, +); + type FormatArgs = [ts.Node?] | [number, MinimalSourceFile]; /** @@ -48,6 +65,13 @@ type FormatArgs = [ts.Node?] | [number, MinimalSourceFile]; * all the required utility functions. */ export class Logger { + /** + * Translation utility for internationalization. + * @privateRemarks + * This is fully initialized by the application during bootstrapping. + */ + i18n: TranslationProxy = dummyTranslationProxy; + /** * How many error messages have been logged? */ @@ -106,7 +130,7 @@ export class Logger { } /** Log the given info message. */ - info(text: string) { + info(text: IfInternal) { this.log(this.addContext(text, LogLevel.Info), LogLevel.Info); } @@ -115,8 +139,12 @@ export class Logger { * * @param text The warning that should be logged. */ - warn(text: string, node?: ts.Node): void; - warn(text: string, pos: number, file: MinimalSourceFile): void; + warn(text: IfInternal, node?: ts.Node): void; + warn( + text: IfInternal, + pos: number, + file: MinimalSourceFile, + ): void; warn(text: string, ...args: FormatArgs): void { const text2 = this.addContext(text, LogLevel.Warn, ...args); if (this.seenWarnings.has(text2) && !isDebugging()) return; @@ -129,8 +157,12 @@ export class Logger { * * @param text The error that should be logged. */ - error(text: string, node?: ts.Node): void; - error(text: string, pos: number, file: MinimalSourceFile): void; + error(text: IfInternal, node?: ts.Node): void; + error( + text: IfInternal, + pos: number, + file: MinimalSourceFile, + ): void; error(text: string, ...args: FormatArgs) { const text2 = this.addContext(text, LogLevel.Error, ...args); if (this.seenErrors.has(text2) && !isDebugging()) return; @@ -138,17 +170,6 @@ export class Logger { this.log(text2, LogLevel.Error); } - /** @internal */ - deprecated(text: string, addStack = true) { - if (addStack) { - const stack = new Error().stack?.split("\n"); - if (stack && stack.length >= 4) { - text = text + "\n" + stack[3]; - } - } - this.warn(text); - } - /** * Print a log message. * diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index ac9382afe..afac9a70d 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -146,7 +146,6 @@ export interface TypeDocOptionMap { includeCategories: boolean; includeGroups: boolean; includeFolders: boolean; - fullTree: boolean; }; visibilityFilters: ManuallyValidatedOption<{ protected?: boolean; diff --git a/src/lib/utils/options/readers/arguments.ts b/src/lib/utils/options/readers/arguments.ts index 63e8b6dbd..dc5845a45 100644 --- a/src/lib/utils/options/readers/arguments.ts +++ b/src/lib/utils/options/readers/arguments.ts @@ -2,6 +2,7 @@ import { ok } from "assert"; import type { OptionsReader, Options } from ".."; import type { Logger } from "../../loggers"; import { ParameterType } from "../declaration"; +import type { TranslatedString } from "../../../internationalization/internationalization"; const ARRAY_OPTION_TYPES = new Set([ ParameterType.Array, @@ -38,7 +39,7 @@ export class ArgumentsReader implements OptionsReader { options.setValue(name, value); } catch (err) { ok(err instanceof Error); - logger.error(err.message); + logger.error(err.message as TranslatedString); } }; @@ -51,7 +52,9 @@ export class ArgumentsReader implements OptionsReader { if (decl) { if (decl.configFileOnly) { logger.error( - `The '${decl.name}' option can only be specified via a config file.`, + logger.i18n.option_0_can_only_be_specified_by_config_file( + decl.name, + ), ); continue; } @@ -80,7 +83,9 @@ export class ArgumentsReader implements OptionsReader { if (index === this.args.length) { // Only boolean values have optional values. logger.warn( - `--${decl.name} expected a value, but none was given as an argument`, + logger.i18n.option_0_expected_a_value_but_none_provided( + decl.name, + ), ); } trySet(decl.name, this.args[index]); @@ -112,9 +117,10 @@ export class ArgumentsReader implements OptionsReader { } logger.error( - `Unknown option: ${name}, you may have meant:\n\t${options - .getSimilarOptions(name) - .join("\n\t")}`, + logger.i18n.unknown_option_0_may_have_meant_1( + name, + options.getSimilarOptions(name).join("\n\t"), + ), ); index++; } diff --git a/src/lib/utils/options/readers/package-json.ts b/src/lib/utils/options/readers/package-json.ts index 570230a4f..b4d5c2296 100644 --- a/src/lib/utils/options/readers/package-json.ts +++ b/src/lib/utils/options/readers/package-json.ts @@ -5,6 +5,7 @@ import { ok } from "assert"; import { nicePath } from "../../paths"; import { discoverPackageJson } from "../../fs"; import { dirname } from "path"; +import type { TranslatedString } from "../../../internationalization/internationalization"; export class PackageJsonReader implements OptionsReader { // Should run after the TypeDoc config reader but before the TS config @@ -25,11 +26,7 @@ export class PackageJsonReader implements OptionsReader { const { file, content } = result; if ("typedoc" in content) { - logger.warn( - `The 'typedoc' key in ${nicePath( - file, - )} was used by the legacy-packages entryPointStrategy and will be ignored.`, - ); + logger.warn(logger.i18n.typedoc_key_in_0_ignored(nicePath(file))); } const optsKey = "typedocOptions"; @@ -40,9 +37,7 @@ export class PackageJsonReader implements OptionsReader { const opts = content[optsKey]; if (opts === null || typeof opts !== "object") { logger.error( - `Failed to parse the "typedocOptions" field in ${nicePath( - file, - )}, ensure it exists and contains an object.`, + logger.i18n.typedoc_options_must_be_object_in_0(nicePath(file)), ); return; } @@ -52,7 +47,7 @@ export class PackageJsonReader implements OptionsReader { container.setValue(opt as never, val as never, dirname(file)); } catch (err) { ok(err instanceof Error); - logger.error(err.message); + logger.error(err.message as TranslatedString); } } } diff --git a/src/lib/utils/options/readers/tsconfig.ts b/src/lib/utils/options/readers/tsconfig.ts index a3e19a036..e89125ccd 100644 --- a/src/lib/utils/options/readers/tsconfig.ts +++ b/src/lib/utils/options/readers/tsconfig.ts @@ -26,6 +26,7 @@ import { getTypeDocOptionsFromTsConfig, readTsConfig, } from "../../tsconfig"; +import type { TranslatedString } from "../../../internationalization/internationalization"; function isSupportForTags(obj: unknown): obj is Record<`@${string}`, boolean> { return ( @@ -83,7 +84,7 @@ export class TSConfigReader implements OptionsReader { // If the user didn't give us this option, we shouldn't complain about not being able to find it. if (container.isSet("tsconfig")) { logger.error( - `The tsconfig file ${nicePath(file)} does not exist`, + logger.i18n.tsconfig_file_0_does_not_exist(nicePath(file)), ); } return; @@ -105,18 +106,11 @@ export class TSConfigReader implements OptionsReader { const typedocOptions = getTypeDocOptionsFromTsConfig(fileToRead); if (typedocOptions.options) { - logger.error( - [ - "typedocOptions in tsconfig file specifies an option file to read but the option", - "file has already been read. This is likely a misconfiguration.", - ].join(" "), - ); + logger.error(logger.i18n.tsconfig_file_specifies_options_file()); delete typedocOptions.options; } if (typedocOptions.tsconfig) { - logger.error( - "typedocOptions in tsconfig file may not specify a tsconfig file to read", - ); + logger.error(logger.i18n.tsconfig_file_specifies_tsconfig_file()); delete typedocOptions.tsconfig; } @@ -135,7 +129,7 @@ export class TSConfigReader implements OptionsReader { ); } catch (error) { ok(error instanceof Error); - logger.error(error.message); + logger.error(error.message as TranslatedString); } } } @@ -156,8 +150,9 @@ export class TSConfigReader implements OptionsReader { ).filter((opt) => container.isSet(opt)); if (overwritten.length) { logger.warn( - `The ${overwritten.join(", ")} defined in typedoc.json will ` + - "be overwritten by configuration in tsdoc.json.", + logger.i18n.tags_0_defined_in_typedoc_json_overwritten_by_tsdoc_json( + overwritten.join(", "), + ), ); } @@ -199,9 +194,7 @@ export class TSConfigReader implements OptionsReader { private readTsDoc(logger: Logger, path: string): TsDocSchema | undefined { if (this.seenTsdocPaths.has(path)) { logger.error( - `Circular reference encountered for "extends" field of ${nicePath( - path, - )}`, + logger.i18n.circular_reference_extends_0(nicePath(path)), ); return; } @@ -213,16 +206,12 @@ export class TSConfigReader implements OptionsReader { ); if (error) { - logger.error( - `Failed to read tsdoc.json file at ${nicePath(path)}.`, - ); + logger.error(logger.i18n.failed_read_tsdoc_json_0(nicePath(path))); return; } if (!validate(tsDocSchema, config)) { - logger.error( - `The file ${nicePath(path)} is not a valid tsdoc.json file.`, - ); + logger.error(logger.i18n.invalid_tsdoc_json_0(nicePath(path))); return; } @@ -236,9 +225,10 @@ export class TSConfigReader implements OptionsReader { resolvedPath = resolver.resolve(extendedPath); } catch { logger.error( - `Failed to resolve ${extendedPath} to a file in ${nicePath( - path, - )}`, + logger.i18n.failed_resolve_0_to_file_in_1( + extendedPath, + nicePath(path), + ), ); return; } diff --git a/src/lib/utils/options/readers/typedoc.ts b/src/lib/utils/options/readers/typedoc.ts index 6344ca81a..aa5b0bb77 100644 --- a/src/lib/utils/options/readers/typedoc.ts +++ b/src/lib/utils/options/readers/typedoc.ts @@ -10,6 +10,7 @@ import { nicePath, normalizePath } from "../../paths"; import { isFile } from "../../fs"; import { createRequire } from "module"; import { pathToFileURL } from "url"; +import type { TranslatedString } from "../../../internationalization/internationalization"; /** * Obtains option values from typedoc.json @@ -37,7 +38,7 @@ export class TypeDocReader implements OptionsReader { if (!file) { if (container.isSet("options")) { logger.error( - `The options file ${nicePath(path)} does not exist.`, + logger.i18n.options_file_0_does_not_exist(nicePath(path)), ); } return; @@ -61,9 +62,7 @@ export class TypeDocReader implements OptionsReader { ) { if (seen.has(file)) { logger.error( - `Tried to load the options file ${nicePath( - file, - )} multiple times.`, + logger.i18n.circular_reference_extends_0(nicePath(file)), ); return; } @@ -77,9 +76,7 @@ export class TypeDocReader implements OptionsReader { if (readResult.error) { logger.error( - `Failed to parse ${nicePath( - file, - )}, ensure it exists and contains an object.`, + logger.i18n.failed_read_options_file_0(nicePath(file)), ); return; } else { @@ -102,9 +99,12 @@ export class TypeDocReader implements OptionsReader { } } catch (error) { logger.error( - `Failed to read ${nicePath(file)}: ${ - error instanceof Error ? error.message : error - }`, + logger.i18n.failed_read_options_file_0(nicePath(file)), + ); + logger.error( + String( + error instanceof Error ? error.message : error, + ) as TranslatedString, ); return; } @@ -112,7 +112,7 @@ export class TypeDocReader implements OptionsReader { if (typeof fileContent !== "object" || !fileContent) { logger.error( - `The root value of ${nicePath(file)} is not an object.`, + logger.i18n.failed_read_options_file_0(nicePath(file)), ); return; } @@ -130,9 +130,10 @@ export class TypeDocReader implements OptionsReader { resolvedParent = resolver.resolve(extendedFile); } catch { logger.error( - `Failed to resolve ${extendedFile} to a file in ${nicePath( - file, - )}`, + logger.i18n.failed_resolve_0_to_file_in_1( + extendedFile, + nicePath(file), + ), ); continue; } @@ -150,7 +151,7 @@ export class TypeDocReader implements OptionsReader { ); } catch (error) { ok(error instanceof Error); - logger.error(error.message); + logger.error(error.message as TranslatedString); } } } diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index ddd000fbe..abd9a535d 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -476,7 +476,6 @@ export function addTypeDocOptions(options: Pick) { includeCategories: false, includeGroups: false, includeFolders: true, - fullTree: false, }, }); diff --git a/src/lib/utils/package-manifest.ts b/src/lib/utils/package-manifest.ts index 00c70f2fe..bd390acc3 100644 --- a/src/lib/utils/package-manifest.ts +++ b/src/lib/utils/package-manifest.ts @@ -29,7 +29,9 @@ export function loadPackageManifest( ): Record | undefined { const packageJson: unknown = JSON.parse(readFile(packageJsonPath)); if (typeof packageJson !== "object" || !packageJson) { - logger.error(`The file ${packageJsonPath} is not an object.`); + logger.error( + logger.i18n.file_0_not_an_object(nicePath(packageJsonPath)), + ); return undefined; } return packageJson as Record; @@ -66,7 +68,7 @@ function getPackagePaths( } /** - * Given a list of (potentially wildcarded) package paths, + * Given a list of (potentially wildcard containing) package paths, * return all the actual package folders found. */ export function expandPackages( @@ -81,35 +83,34 @@ export function expandPackages( // be dealing with either a root or a leaf. So let's do this recursively, // as it actually is simpler from an implementation perspective anyway. return workspaces.flatMap((workspace) => { - const globbedPackageJsonPaths = glob( + const expandedPackageJsonPaths = glob( resolve(packageJsonDir, workspace, "package.json"), resolve(packageJsonDir), ); - if (globbedPackageJsonPaths.length === 0) { + if (expandedPackageJsonPaths.length === 0) { logger.warn( - `The entrypoint glob ${nicePath( - workspace, - )} did not match any directories containing package.json.`, + logger.i18n.entry_point_0_did_not_match_any_packages( + nicePath(workspace), + ), ); } else { logger.verbose( `Expanded ${nicePath( workspace, - )} to:\n\t${globbedPackageJsonPaths + )} to:\n\t${expandedPackageJsonPaths .map(nicePath) .join("\n\t")}`, ); } - return globbedPackageJsonPaths.flatMap((packageJsonPath) => { + return expandedPackageJsonPaths.flatMap((packageJsonPath) => { if (matchesAny(exclude, dirname(packageJsonPath))) { return []; } const packageJson = loadPackageManifest(logger, packageJsonPath); if (packageJson === undefined) { - logger.error(`Failed to load ${packageJsonPath}`); return []; } const packagePaths = getPackagePaths(packageJson); diff --git a/src/lib/utils/plugins.ts b/src/lib/utils/plugins.ts index 25019cb72..62a65d7ce 100644 --- a/src/lib/utils/plugins.ts +++ b/src/lib/utils/plugins.ts @@ -3,6 +3,7 @@ import { pathToFileURL } from "url"; import type { Application } from "../application"; import { nicePath } from "./paths"; +import type { TranslatedString } from "../internationalization/internationalization"; export async function loadPlugins( app: Application, @@ -32,18 +33,20 @@ export async function loadPlugins( if (typeof initFunction === "function") { await initFunction(app); - app.logger.info(`Loaded plugin ${pluginDisplay}`); + app.logger.info(app.i18n.loaded_plugin_0(pluginDisplay)); } else { app.logger.error( - `Invalid structure in plugin ${pluginDisplay}, no load function found.`, + app.i18n.invalid_plugin_0_missing_load_function( + pluginDisplay, + ), ); } } catch (error) { app.logger.error( - `The plugin ${pluginDisplay} could not be loaded.`, + app.i18n.plugin_0_could_not_be_loaded(pluginDisplay), ); if (error instanceof Error && error.stack) { - app.logger.error(error.stack); + app.logger.error(error.stack as TranslatedString); } } } diff --git a/src/lib/validation/documentation.ts b/src/lib/validation/documentation.ts index a40a73e68..427b845d8 100644 --- a/src/lib/validation/documentation.ts +++ b/src/lib/validation/documentation.ts @@ -92,11 +92,11 @@ export function validateDocumentation( } logger.warn( - `${ref.getFriendlyFullName()} (${ - ReflectionKind[ref.kind] - }), defined in ${nicePath( - symbolId.fileName, - )}, does not have any documentation.`, + logger.i18n.reflection_0_kind_1_defined_in_2_does_not_have_any_documentation( + ref.getFriendlyFullName(), + ReflectionKind[ref.kind], + nicePath(symbolId.fileName), + ), ); } } diff --git a/src/lib/validation/exports.ts b/src/lib/validation/exports.ts index 236ae6f01..b439f0db3 100644 --- a/src/lib/validation/exports.ts +++ b/src/lib/validation/exports.ts @@ -85,9 +85,11 @@ export function validateExports( warned.add(uniqueId!); logger.warn( - `${type.qualifiedName}, defined in ${nicePath( - type.symbolId!.fileName, - )}, is referenced by ${owner.getFullName()} but not included in the documentation.`, + logger.i18n.type_0_defined_in_1_is_referenced_by_2_but_not_included_in_docs( + type.qualifiedName, + nicePath(type.symbolId!.fileName), + owner.getFriendlyFullName(), + ), ); } } @@ -95,8 +97,9 @@ export function validateExports( const unusedIntentional = intentional.getUnused(); if (unusedIntentional.length) { logger.warn( - "The following symbols were marked as intentionally not exported, but were either not referenced in the documentation, or were exported:\n\t" + + logger.i18n.invalid_intentionally_not_exported_symbols_0( unusedIntentional.join("\n\t"), + ), ); } } diff --git a/src/lib/validation/links.ts b/src/lib/validation/links.ts index ca2fb39d4..a98146fa5 100644 --- a/src/lib/validation/links.ts +++ b/src/lib/validation/links.ts @@ -40,7 +40,10 @@ export function validateLinks( )}"`; } logger.warn( - `Failed to resolve link to "${broken}" in comment for ${reflection.getFriendlyFullName()}.${extra}`, + logger.i18n.failed_to_resolve_link_to_0_in_comment_for_1( + broken, + `${reflection.getFriendlyFullName()}.${extra}`, + ), ); } } diff --git a/src/test/TestLogger.ts b/src/test/TestLogger.ts index b1d63a0c8..dfddb9bca 100644 --- a/src/test/TestLogger.ts +++ b/src/test/TestLogger.ts @@ -2,6 +2,10 @@ import { Logger, LogLevel } from "../lib/utils"; import { fail, ok } from "assert"; import ts from "typescript"; import { resolve } from "path"; +import { + Internationalization, + type TranslationProxy, +} from "../lib/internationalization/internationalization"; const levelMap: Record = { [LogLevel.None]: "none: ", @@ -13,6 +17,9 @@ const levelMap: Record = { export class TestLogger extends Logger { messages: string[] = []; + override i18n: TranslationProxy = new Internationalization( + null, + ).createProxy(); reset() { this.resetErrors();