diff --git a/README.md b/README.md index 8da667ec1..68c8bc5ba 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ -# OpenAPI-diff +# OpenAPI-diff Compare two OpenAPI specifications (3.x) and render the difference to HTML plain text, Markdown files, or JSON files. -[![Build](https://github.com/OpenAPITools/openapi-diff/workflows/Main%20Build/badge.svg)](https://github.com/OpenAPITools/openapi-diff/actions?query=branch%3Amaster+workflow%3A"Main+Build") -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=OpenAPITools_openapi-diff&metric=alert_status)](https://sonarcloud.io/dashboard?id=OpenAPITools_openapi-diff) +[Build](https://github.com/OpenAPITools/openapi-diff/actions?query=branch%3Amaster+workflow%3A"Main+Build") +[Quality Gate Status](https://sonarcloud.io/dashboard?id=OpenAPITools_openapi-diff) -[![Maven Central](https://img.shields.io/maven-central/v/org.openapitools.openapidiff/openapi-diff-core)](https://search.maven.org/artifact/org.openapitools.openapidiff/openapi-diff-core) +[Maven Central](https://search.maven.org/artifact/org.openapitools.openapidiff/openapi-diff-core) -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/OpenAPITools/openapi-diff) -[![Join the Slack chat room](https://img.shields.io/badge/Slack-Join%20the%20chat%20room-orange)](https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g) +[Contribute with Gitpod](https://gitpod.io/#https://github.com/OpenAPITools/openapi-diff) +[Join the Slack chat room](https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g) -[![Docker Automated build](https://img.shields.io/docker/automated/openapitools/openapi-diff)](https://hub.docker.com/r/openapitools/openapi-diff) -[![Docker Image Version](https://img.shields.io/docker/v/openapitools/openapi-diff?sort=semver)](https://hub.docker.com/r/openapitools/openapi-diff/tags) +[Docker Automated build](https://hub.docker.com/r/openapitools/openapi-diff) +[Docker Image Version](https://hub.docker.com/r/openapitools/openapi-diff/tags) # Requirements -* Java 8 +- Java 8 # Feature -* Supports OpenAPI spec v3.0. -* In-depth comparison of parameters, responses, endpoint, http method (GET,POST,PUT,DELETE...) -* Supports swagger api Authorization -* Render difference of property with Expression Language -* HTML, Markdown, Asciidoc & JSON render +- Supports OpenAPI spec v3.0. +- In-depth comparison of parameters, responses, endpoint, http method (GET,POST,PUT,DELETE...) +- Supports swagger api Authorization +- Render difference of property with Expression Language +- HTML, Markdown, Asciidoc & JSON render # Maven @@ -38,11 +38,13 @@ Available on [Maven Central](https://search.maven.org/artifact/org.openapitools. ``` # Homebrew + Available for Mac users on [brew](https://formulae.brew.sh/formula/openapi-diff) ```bash brew install openapi-diff ``` + Usage instructions in [Usage -> Command line](#command-line) # Docker @@ -211,6 +213,7 @@ ChangedOpenApi diff = OpenApiCompare.fromLocations(oldSpec, newSpec, null, optio ### Render difference --- + #### HTML ```java @@ -500,7 +503,8 @@ openapi-diff is released under the Apache License 2.0. # Thanks -* Adarsh Sharma / [adarshsharma](https://github.com/adarshsharma) -* Quentin Desramé / [quen2404](https://github.com/quen2404) -* [Sayi](https://github.com/Sayi) for his project [swagger-diff](https://github.com/Sayi/swagger-diff) - which was a source of inspiration for this tool +- Adarsh Sharma / [adarshsharma](https://github.com/adarshsharma) +- Quentin Desramé / [quen2404](https://github.com/quen2404) +- [Sayi](https://github.com/Sayi) for his project [swagger-diff](https://github.com/Sayi/swagger-diff) +which was a source of inspiration for this tool + diff --git a/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java b/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java index 85f125d78..421b543e4 100644 --- a/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java +++ b/cli/src/main/java/org/openapitools/openapidiff/cli/Main.java @@ -22,6 +22,7 @@ import org.openapitools.openapidiff.core.output.AsciidocRender; import org.openapitools.openapidiff.core.output.ConsoleRender; import org.openapitools.openapidiff.core.output.HtmlRender; +import org.openapitools.openapidiff.core.output.I18n; import org.openapitools.openapidiff.core.output.JsonRender; import org.openapitools.openapidiff.core.output.MarkdownRender; import org.slf4j.Logger; @@ -143,6 +144,13 @@ public static void main(String... args) { .argName("file") .desc("export diff as json in given file") .build()); + options.addOption( + Option.builder() + .longOpt("lang") + .hasArg() + .argName("language") + .desc("output language (en, zh-Hant, zh-CN). Default: en") + .build()); // create the parser CommandLineParser parser = new DefaultParser(); @@ -155,9 +163,13 @@ public static void main(String... args) { } if (line.hasOption("version") || line.hasOption("v")) { String version = Main.class.getPackage().getImplementationVersion(); - System.out.println("openapi-diff version: " + (version != null ? version : "DEV")); + System.out.println( + I18n.getMessage("cli.version.prefix") + " " + (version != null ? version : "DEV")); System.exit(0); } + if (line.hasOption("lang")) { + I18n.setLocale(I18n.parseLocale(line.getOptionValue("lang"))); + } String logLevel = "ERROR"; if (line.hasOption("off")) { logLevel = "OFF"; @@ -185,10 +197,7 @@ public static void main(String... args) { && !logLevel.equalsIgnoreCase("WARN") && !logLevel.equalsIgnoreCase("ERROR") && !logLevel.equalsIgnoreCase("OFF")) { - throw new ParseException( - String.format( - "Invalid log level. Expected: [TRACE, DEBUG, INFO, WARN, ERROR, OFF]. Given: %s", - logLevel)); + throw new ParseException(I18n.getMessage("cli.invalid.log.level", logLevel)); } } if (line.hasOption("state")) { @@ -199,7 +208,7 @@ public static void main(String... args) { root.setLevel(Level.toLevel(logLevel)); if (line.getArgList().size() < 2) { - throw new ParseException("Missing arguments"); + throw new ParseException(I18n.getMessage("cli.missing.arguments")); } String oldPath = line.getArgList().get(0); String newPath = line.getArgList().get(1); @@ -223,7 +232,8 @@ public static void main(String... args) { for (String propKeyAndVal : configProps) { String[] split = propKeyAndVal.split(":"); if (split.length != 2 || split[0].isEmpty() || split[1].isEmpty()) { - throw new IllegalArgumentException("--config-prop unexpected format: " + propKeyAndVal); + throw new IllegalArgumentException( + I18n.getMessage("cli.config.prop.unexpected.format") + " " + propKeyAndVal); } optionBuilder.configProperty(split[0], split[1]); } @@ -283,12 +293,13 @@ public static void main(String... args) { } } catch (ParseException e) { // oops, something went wrong - System.err.println("Parsing failed. Reason: " + e.getMessage()); + System.err.println(I18n.getMessage("cli.parsing.failed") + " " + e.getMessage()); printHelp(options); System.exit(2); } catch (Exception e) { System.err.println( - "Unexpected exception. Reason: " + I18n.getMessage("cli.unexpected.exception") + + " " + e.getMessage() + "\n" + ExceptionUtils.getStackTrace(e)); diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java index 895838472..f439ecf6e 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/AsciidocRender.java @@ -46,7 +46,7 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { diff.getNewSpecOpenApi().getInfo().getVersion())); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, System.lineSeparator()); - safelyAppend(outputStreamWriter, "NOTE: No differences. Specifications are equivalent"); + safelyAppend(outputStreamWriter, I18n.getMessage("note.no.differences")); } else { safelyAppend( outputStreamWriter, @@ -63,13 +63,13 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { safelyAppend(outputStreamWriter, System.lineSeparator()); List newEndpoints = diff.getNewEndpoints(); - listEndpoints(newEndpoints, "What's New", outputStreamWriter); + listEndpoints(newEndpoints, I18n.getMessage("whats.new"), outputStreamWriter); List missingEndpoints = diff.getMissingEndpoints(); - listEndpoints(missingEndpoints, "What's Deleted", outputStreamWriter); + listEndpoints(missingEndpoints, I18n.getMessage("whats.deleted"), outputStreamWriter); List deprecatedEndpoints = diff.getDeprecatedEndpoints(); - listEndpoints(deprecatedEndpoints, "What's Deprecated", outputStreamWriter); + listEndpoints(deprecatedEndpoints, I18n.getMessage("whats.deprecated"), outputStreamWriter); List changedOperations = diff.getChangedOperations(); ol_changed(changedOperations, outputStreamWriter); @@ -78,8 +78,8 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { safelyAppend( outputStreamWriter, diff.isCompatible() - ? "NOTE: API changes are backward compatible" - : "WARNING: API changes broke backward compatibility"); + ? I18n.getMessage("note.backward.compatible") + : I18n.getMessage("warning.broke.compatibility")); safelyAppend(outputStreamWriter, System.lineSeparator()); } try { @@ -94,7 +94,7 @@ private void ol_changed( if (null == operations || operations.isEmpty()) { return; } - safelyAppend(outputStreamWriter, title("What's Changed", 2)); + safelyAppend(outputStreamWriter, title(I18n.getMessage("whats.changed"), 2)); safelyAppend(outputStreamWriter, System.lineSeparator()); for (ChangedOperation operation : operations) { String pathUrl = operation.getPathUrl(); @@ -105,30 +105,33 @@ private void ol_changed( safelyAppend(outputStreamWriter, itemEndpoint(method, pathUrl, desc)); safelyAppend(outputStreamWriter, System.lineSeparator()); if (result(operation.getOperationId()).isDifferent()) { - safelyAppend(outputStreamWriter, "* Operation ID:"); + safelyAppend(outputStreamWriter, "* " + I18n.getMessage("operation.id") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend( outputStreamWriter, String.format( - "** Changed %s to %s", - operation.getOperationId().getLeft(), operation.getOperationId().getRight())); + "** %s %s %s %s", + I18n.getMessage("action.changed"), + operation.getOperationId().getLeft(), + I18n.getMessage("to"), + operation.getOperationId().getRight())); safelyAppend(outputStreamWriter, System.lineSeparator()); } if (result(operation.getParameters()).isDifferent()) { - safelyAppend(outputStreamWriter, "* Parameter:"); + safelyAppend(outputStreamWriter, "* " + I18n.getMessage("parameter") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, ul_param(operation.getParameters())); safelyAppend(outputStreamWriter, System.lineSeparator()); } if (operation.resultRequestBody().isDifferent()) { - safelyAppend(outputStreamWriter, "* Request:"); + safelyAppend(outputStreamWriter, "* " + I18n.getMessage("request") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend( outputStreamWriter, ul_content(operation.getRequestBody().getContent(), true, 2)); safelyAppend(outputStreamWriter, System.lineSeparator()); } if (operation.resultApiResponses().isDifferent()) { - safelyAppend(outputStreamWriter, "* Return Type:"); + safelyAppend(outputStreamWriter, "* " + I18n.getMessage("return.type") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, ul_response(operation.getApiResponses())); safelyAppend(outputStreamWriter, System.lineSeparator()); @@ -142,13 +145,15 @@ private String ul_response(ChangedApiResponse changedApiResponse) { Map changedResponses = changedApiResponse.getChanged(); StringBuilder sb = new StringBuilder(); for (String propName : addResponses.keySet()) { - sb.append(itemResponse("** Add ", propName)); + sb.append(itemResponse("** " + I18n.getMessage("action.add") + " ", propName)); } for (String propName : delResponses.keySet()) { - sb.append(itemResponse("** Deleted ", propName)); + sb.append(itemResponse("** " + I18n.getMessage("action.deleted") + " ", propName)); } for (Entry entry : changedResponses.entrySet()) { - sb.append(itemChangedResponse("** Changed ", entry.getKey(), entry.getValue())); + sb.append( + itemChangedResponse( + "** " + I18n.getMessage("action.changed") + " ", entry.getKey(), entry.getValue())); } return sb.toString(); } @@ -165,7 +170,9 @@ private String itemResponse(String title, String code) { private String itemChangedResponse(String title, String contentType, ChangedResponse response) { return itemResponse(title, contentType) - + "** Media types:" + + "** " + + I18n.getMessage("media.types") + + ":" + System.lineSeparator() + ul_content(response.getContent(), false, 3); } @@ -176,15 +183,19 @@ private String ul_content(ChangedContent changedContent, boolean isRequest, int return sb.toString(); } for (String propName : changedContent.getIncreased().keySet()) { - sb.append(itemContent("Added ", propName, indent)); + sb.append(itemContent(I18n.getMessage("action.added") + " ", propName, indent)); } for (String propName : changedContent.getMissing().keySet()) { - sb.append(itemContent("Deleted ", propName, indent)); + sb.append(itemContent(I18n.getMessage("action.deleted") + " ", propName, indent)); } for (String propName : changedContent.getChanged().keySet()) { sb.append( itemContent( - "Changed ", propName, indent, changedContent.getChanged().get(propName), isRequest)); + I18n.getMessage("action.changed") + " ", + propName, + indent, + changedContent.getChanged().get(propName), + isRequest)); } return sb.toString(); } @@ -201,8 +212,11 @@ private String itemContent( boolean isRequest) { StringBuilder sb = new StringBuilder(); sb.append(itemContent(title, contentType, indent)) - .append(itemContent("Schema:", "", indent)) - .append(changedMediaType.isCompatible() ? "Backward compatible" : "Broken compatibility") + .append(itemContent(I18n.getMessage("schema") + ":", "", indent)) + .append( + changedMediaType.isCompatible() + ? I18n.getMessage("backward.compatible") + : I18n.getMessage("broken.compatibility")) .append(System.lineSeparator()); if (!changedMediaType.isCompatible() && changedMediaType.getSchema() != null) { sb.append(incompatibilities(changedMediaType.getSchema())); @@ -221,13 +235,17 @@ private String incompatibilities(String propName, final ChangedSchema schema) { } if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) { String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); - sb.append(property(propName, "Changed property type", type)); + sb.append(property(propName, I18n.getMessage("changed.property.type"), type)); sb.append(System.lineSeparator()); sb.append(System.lineSeparator()); } String prefix = propName.isEmpty() ? "" : propName + "."; sb.append( - properties(prefix, "Missing property", schema.getMissingProperties(), schema.getContext())); + properties( + prefix, + I18n.getMessage("missing.property"), + schema.getMissingProperties(), + schema.getContext())); schema .getChangedProperties() .forEach((name, property) -> sb.append(incompatibilities(prefix + name, property))); @@ -286,26 +304,34 @@ private String ul_param(ChangedParameters changedParameters) { List changed = changedParameters.getChanged(); StringBuilder sb = new StringBuilder(); for (Parameter param : addParameters) { - sb.append(itemParam("** Add ", param)); + sb.append(itemParam("** " + I18n.getMessage("action.add") + " ", param)); } for (ChangedParameter param : changed) { sb.append(li_changedParam(param)); } for (Parameter param : delParameters) { - sb.append(itemParam("** Delete ", param)); + sb.append(itemParam("** " + I18n.getMessage("action.delete") + " ", param)); } return sb.toString(); } private String itemParam(String title, Parameter param) { - return title + param.getName() + " in " + param.getIn() + System.lineSeparator(); + return title + + param.getName() + + " " + + I18n.getMessage("in") + + " " + + param.getIn() + + System.lineSeparator(); } private String li_changedParam(ChangedParameter changeParam) { if (changeParam.isDeprecated()) { - return itemParam("** Deprecated ", changeParam.getNewParameter()); + return itemParam( + "** " + I18n.getMessage("action.deprecated") + " ", changeParam.getNewParameter()); } else { - return itemParam("** Changed ", changeParam.getNewParameter()); + return itemParam( + "** " + I18n.getMessage("action.changed") + " ", changeParam.getNewParameter()); } } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java index 2b8607232..8be377638 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/ConsoleRender.java @@ -28,33 +28,33 @@ public class ConsoleRender implements Render { public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { this.diff = diff; if (diff.isUnchanged()) { - safelyAppend(outputStreamWriter, "No differences. Specifications are equivalent"); + safelyAppend(outputStreamWriter, I18n.getMessage("no.differences")); } else { - safelyAppend(outputStreamWriter, bigTitle("Api Change Log")); + safelyAppend(outputStreamWriter, bigTitle(I18n.getMessage("api.change.log"))); safelyAppend( outputStreamWriter, - StringUtils.center(diff.getNewSpecOpenApi().getInfo().getTitle(), LINE_LENGTH)); + centerCjk(diff.getNewSpecOpenApi().getInfo().getTitle(), LINE_LENGTH)); safelyAppend(outputStreamWriter, System.lineSeparator()); List newEndpoints = diff.getNewEndpoints(); - listEndpoints(newEndpoints, "What's New", outputStreamWriter); + listEndpoints(newEndpoints, I18n.getMessage("whats.new"), outputStreamWriter); List missingEndpoints = diff.getMissingEndpoints(); - listEndpoints(missingEndpoints, "What's Deleted", outputStreamWriter); + listEndpoints(missingEndpoints, I18n.getMessage("whats.deleted"), outputStreamWriter); List deprecatedEndpoints = diff.getDeprecatedEndpoints(); - listEndpoints(deprecatedEndpoints, "What's Deprecated", outputStreamWriter); + listEndpoints(deprecatedEndpoints, I18n.getMessage("whats.deprecated"), outputStreamWriter); List changedOperations = diff.getChangedOperations(); ol_changed(changedOperations, outputStreamWriter); - safelyAppend(outputStreamWriter, title("Result")); + safelyAppend(outputStreamWriter, title(I18n.getMessage("result"))); safelyAppend( outputStreamWriter, - StringUtils.center( + centerCjk( diff.isCompatible() - ? "API changes are backward compatible" - : "API changes broke backward compatibility", + ? I18n.getMessage("api.changes.backward.compatible") + : I18n.getMessage("api.changes.broke.compatibility"), LINE_LENGTH)); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, separator('-')); @@ -71,7 +71,7 @@ private void ol_changed( if (null == operations || operations.isEmpty()) { return; } - safelyAppend(outputStreamWriter, title("What's Changed")); + safelyAppend(outputStreamWriter, title(I18n.getMessage("whats.changed"))); for (ChangedOperation operation : operations) { String pathUrl = operation.getPathUrl(); String method = operation.getHttpMethod().toString(); @@ -82,25 +82,25 @@ private void ol_changed( if (result(operation.getOperationId()).isDifferent()) { safelyAppend(outputStreamWriter, StringUtils.repeat(' ', 2)); - safelyAppend(outputStreamWriter, "Operation ID:"); + safelyAppend(outputStreamWriter, I18n.getMessage("operation.id") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, ul_operation_id(operation.getOperationId())); } if (result(operation.getParameters()).isDifferent()) { safelyAppend(outputStreamWriter, StringUtils.repeat(' ', 2)); - safelyAppend(outputStreamWriter, "Parameter:"); + safelyAppend(outputStreamWriter, I18n.getMessage("parameter") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, ul_param(operation.getParameters())); } if (operation.resultRequestBody().isDifferent()) { safelyAppend(outputStreamWriter, StringUtils.repeat(' ', 2)); - safelyAppend(outputStreamWriter, "Request:"); + safelyAppend(outputStreamWriter, I18n.getMessage("request") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, ul_content(operation.getRequestBody().getContent(), true)); } if (operation.resultApiResponses().isDifferent()) { safelyAppend(outputStreamWriter, StringUtils.repeat(' ', 2)); - safelyAppend(outputStreamWriter, "Return Type:"); + safelyAppend(outputStreamWriter, I18n.getMessage("return.type") + ":"); safelyAppend(outputStreamWriter, System.lineSeparator()); safelyAppend(outputStreamWriter, ul_response(operation.getApiResponses())); } @@ -113,13 +113,15 @@ private String ul_response(ChangedApiResponse changedApiResponse) { Map changedResponses = changedApiResponse.getChanged(); StringBuilder sb = new StringBuilder(); for (String propName : addResponses.keySet()) { - sb.append(itemResponse("Add ", propName)); + sb.append(itemResponse(I18n.getMessage("action.add") + " ", propName)); } for (String propName : delResponses.keySet()) { - sb.append(itemResponse("Deleted ", propName)); + sb.append(itemResponse(I18n.getMessage("action.deleted") + " ", propName)); } for (Entry entry : changedResponses.entrySet()) { - sb.append(itemChangedResponse("Changed ", entry.getKey(), entry.getValue())); + sb.append( + itemChangedResponse( + I18n.getMessage("action.changed") + " ", entry.getKey(), entry.getValue())); } return sb.toString(); } @@ -143,7 +145,8 @@ private String itemResponse(String title, String code) { private String itemChangedResponse(String title, String contentType, ChangedResponse response) { return itemResponse(title, contentType) + StringUtils.repeat(' ', 6) - + "Media types:" + + I18n.getMessage("media.types") + + ":" + System.lineSeparator() + ul_content(response.getContent(), false); } @@ -154,14 +157,18 @@ private String ul_content(ChangedContent changedContent, boolean isRequest) { return sb.toString(); } for (String propName : changedContent.getIncreased().keySet()) { - sb.append(itemContent("Added ", propName)); + sb.append(itemContent(I18n.getMessage("action.added") + " ", propName)); } for (String propName : changedContent.getMissing().keySet()) { - sb.append(itemContent("Deleted ", propName)); + sb.append(itemContent(I18n.getMessage("action.deleted") + " ", propName)); } for (String propName : changedContent.getChanged().keySet()) { sb.append( - itemContent("Changed ", propName, changedContent.getChanged().get(propName), isRequest)); + itemContent( + I18n.getMessage("action.changed") + " ", + propName, + changedContent.getChanged().get(propName), + isRequest)); } return sb.toString(); } @@ -175,8 +182,11 @@ private String itemContent( StringBuilder sb = new StringBuilder(); sb.append(itemContent(title, contentType)) .append(StringUtils.repeat(' ', 10)) - .append("Schema: ") - .append(changedMediaType.isCompatible() ? "Backward compatible" : "Broken compatibility") + .append(I18n.getMessage("schema") + ": ") + .append( + changedMediaType.isCompatible() + ? I18n.getMessage("backward.compatible") + : I18n.getMessage("broken.compatibility")) .append(System.lineSeparator()); if (!changedMediaType.isCompatible() && changedMediaType.getSchema() != null) { sb.append(incompatibilities(changedMediaType.getSchema())); @@ -195,11 +205,15 @@ private String incompatibilities(String propName, final ChangedSchema schema) { } if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) { String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); - sb.append(property(propName, "Changed property type", type)); + sb.append(property(propName, I18n.getMessage("changed.property.type"), type)); } String prefix = propName.isEmpty() ? "" : propName + "."; sb.append( - properties(prefix, "Missing property", schema.getMissingProperties(), schema.getContext())); + properties( + prefix, + I18n.getMessage("missing.property"), + schema.getMissingProperties(), + schema.getContext())); schema .getChangedProperties() .forEach((name, property) -> sb.append(incompatibilities(prefix + name, property))); @@ -258,13 +272,13 @@ private String ul_param(ChangedParameters changedParameters) { List changed = changedParameters.getChanged(); StringBuilder sb = new StringBuilder(); for (Parameter param : addParameters) { - sb.append(itemParam("Add ", param)); + sb.append(itemParam(I18n.getMessage("action.add") + " ", param)); } for (ChangedParameter param : changed) { sb.append(li_changedParam(param)); } for (Parameter param : delParameters) { - sb.append(itemParam("Delete ", param)); + sb.append(itemParam(I18n.getMessage("action.delete") + " ", param)); } return sb.toString(); } @@ -275,16 +289,18 @@ private String itemParam(String title, Parameter param) { + "- " + title + param.getName() - + " in " + + " " + + I18n.getMessage("in") + + " " + param.getIn() + System.lineSeparator(); } private String li_changedParam(ChangedParameter changeParam) { if (changeParam.isDeprecated()) { - return itemParam("Deprecated ", changeParam.getNewParameter()); + return itemParam(I18n.getMessage("action.deprecated") + " ", changeParam.getNewParameter()); } else { - return itemParam("Changed ", changeParam.getNewParameter()); + return itemParam(I18n.getMessage("action.changed") + " ", changeParam.getNewParameter()); } } @@ -309,7 +325,12 @@ private String itemEndpoint(String method, String path, String desc) { } private String ul_operation_id(ChangedOperationId operationId) { - return String.format(" - Changed %s to %s\n", operationId.getLeft(), operationId.getRight()); + return String.format( + " - %s %s %s %s\n", + I18n.getMessage("action.changed"), + operationId.getLeft(), + I18n.getMessage("to"), + operationId.getRight()); } public String renderBody(String ol_new, String ol_miss, String ol_deprec, String ol_changed) { @@ -329,10 +350,54 @@ public String title(String title, char ch) { String little = StringUtils.repeat(ch, 2); return String.format( "%s%s%s%s%n%s", - separator(ch), little, StringUtils.center(title, LINE_LENGTH - 4), little, separator(ch)); + separator(ch), little, centerCjk(title, LINE_LENGTH - 4), little, separator(ch)); } public String separator(char ch) { return StringUtils.repeat(ch, LINE_LENGTH) + System.lineSeparator(); } + + static int displayWidth(String text) { + if (text == null) { + return 0; + } + int width = 0; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (isFullWidth(c)) { + width += 2; + } else { + width += 1; + } + } + return width; + } + + static boolean isFullWidth(char c) { + return (c >= '\u1100' && c <= '\u115F') + || (c >= '\u2E80' && c <= '\u303E') + || (c >= '\u3040' && c <= '\u33BF') + || (c >= '\u3400' && c <= '\u4DBF') + || (c >= '\u4E00' && c <= '\u9FFF') + || (c >= '\uA960' && c <= '\uA97F') + || (c >= '\uAC00' && c <= '\uD7FF') + || (c >= '\uF900' && c <= '\uFAFF') + || (c >= '\uFE30' && c <= '\uFE4F') + || (c >= '\uFF00' && c <= '\uFF60') + || (c >= '\uFFE0' && c <= '\uFFE6'); + } + + static String centerCjk(String text, int width) { + if (text == null) { + text = ""; + } + int textWidth = displayWidth(text); + if (textWidth >= width) { + return text; + } + int totalPadding = width - textWidth; + int leftPadding = totalPadding / 2; + int rightPadding = totalPadding - leftPadding; + return StringUtils.repeat(' ', leftPadding) + text + StringUtils.repeat(' ', rightPadding); + } } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java index 441d1c9dd..6d90c2ffb 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/HtmlRender.java @@ -72,11 +72,15 @@ public class HtmlRender implements Render { protected ChangedOpenApi diff; public HtmlRender() { - this("Api Change Log", "http://deepoove.com/swagger-diff/stylesheets/demo.css"); + this( + I18n.getMessage("api.change.log"), "http://deepoove.com/swagger-diff/stylesheets/demo.css"); } public HtmlRender(boolean showAllChanges) { - this("Api Change Log", "http://deepoove.com/swagger-diff/stylesheets/demo.css", showAllChanges); + this( + I18n.getMessage("api.change.log"), + "http://deepoove.com/swagger-diff/stylesheets/demo.css", + showAllChanges); } public HtmlRender(String title, String linkCss) { @@ -118,7 +122,7 @@ public void renderHtml( OutputStreamWriter outputStreamWriter) { HtmlTag html = html() - .attr("lang", "en") + .attr("lang", I18n.getLocale().toLanguageTag()) .with( head() .with( @@ -131,10 +135,13 @@ public void renderHtml( div() .withClass("article") .with( - div().with(h2("What's New"), hr(), ol_new), - div().with(h2("What's Deleted"), hr(), ol_miss), - div().with(h2("What's Deprecated"), hr(), ol_deprec), - div().with(h2("What's Changed"), hr(), ol_changed)))); + div().with(h2(I18n.getMessage("whats.new")), hr(), ol_new), + div().with(h2(I18n.getMessage("whats.deleted")), hr(), ol_miss), + div() + .with(h2(I18n.getMessage("whats.deprecated")), hr(), ol_deprec), + div() + .with( + h2(I18n.getMessage("whats.changed")), hr(), ol_changed)))); try { FlatHtml flatHtml = FlatHtml.into(outputStreamWriter); @@ -205,24 +212,27 @@ private OlTag ol_changed(List changedOperations) { UlTag ul_detail = ul().withClass("detail"); if (result(changedOperation.getOperationId()).isDifferent()) { ul_detail.with( - li().with(h3("Operation ID")).with(ul_operation_id(changedOperation.getOperationId()))); + li().with(h3(I18n.getMessage("operation.id"))) + .with(ul_operation_id(changedOperation.getOperationId()))); } if (result(changedOperation.getParameters()).isDifferent()) { ul_detail.with( - li().with(h3("Parameters")).with(ul_param(changedOperation.getParameters()))); + li().with(h3(I18n.getMessage("parameters"))) + .with(ul_param(changedOperation.getParameters()))); } if (changedOperation.resultRequestBody().isDifferent()) { ul_detail.with( - li().with(h3("Request")) + li().with(h3(I18n.getMessage("request"))) .with(ul_request(changedOperation.getRequestBody().getContent()))); } if (changedOperation.resultApiResponses().isDifferent()) { ul_detail.with( - li().with(h3("Response")).with(ul_response(changedOperation.getApiResponses()))); + li().with(h3(I18n.getMessage("response"))) + .with(ul_response(changedOperation.getApiResponses()))); } if (showAllChanges && changedOperation.resultSecurityRequirements().isDifferent()) { ul_detail.with( - li().with(h3("Security Requirements")) + li().with(h3(I18n.getMessage("security.requirements"))) .with(ul_securityRequirements(changedOperation.getSecurityRequirements()))); } ol.with( @@ -259,18 +269,18 @@ private UlTag ul_securityRequirements(ChangedSecurityRequirements changedSecurit } private LiTag li_addSecurityRequirement(SecurityRequirement securityRequirement) { - return li().withText("New security requirement : ") + return li().withText(I18n.getMessage("new.security.requirement") + " ") .with(span(null == securityRequirement.toString() ? "" : (securityRequirement.toString()))); } private LiTag li_missingSecurityRequirement(SecurityRequirement securityRequirement) { - return li().withText("Deleted security requirement : ") + return li().withText(I18n.getMessage("deleted.security.requirement") + " ") .with(span(null == securityRequirement.toString() ? "" : (securityRequirement.toString()))); } private LiTag li_changedSecurityRequirement( ChangedSecurityRequirement changedSecurityRequirement) { - return li().withText(String.format("Changed security requirement : ")) + return li().withText(I18n.getMessage("changed.security.requirement") + " ") .with( span( (null == changedSecurityRequirement.getNewSecurityRequirement() @@ -298,21 +308,21 @@ private UlTag ul_response(ChangedApiResponse changedApiResponse) { } private LiTag li_addResponse(String name, ApiResponse response) { - return li().withText(String.format("New response : [%s]", name)) + return li().withText(I18n.getMessage("new.response.code", name)) .with( span(null == response.getDescription() ? "" : ("//" + response.getDescription())) .withClass(COMMENT)); } private LiTag li_missingResponse(String name, ApiResponse response) { - return li().withText(String.format("Deleted response : [%s]", name)) + return li().withText(I18n.getMessage("deleted.response.code", name)) .with( span(null == response.getDescription() ? "" : ("//" + response.getDescription())) .withClass(COMMENT)); } private LiTag li_changedResponse(String name, ChangedResponse response) { - return li().withText(String.format("Changed response : [%s]", name)) + return li().withText(I18n.getMessage("changed.response.code", name)) .with( span((null == response.getNewApiResponse() || null == response.getNewApiResponse().getDescription()) @@ -339,15 +349,15 @@ private UlTag ul_request(ChangedContent changedContent) { } private LiTag li_addRequest(String name, MediaType request) { - return li().withText(String.format("New body: '%s'", name)); + return li().withText(I18n.getMessage("new.body", name)); } private LiTag li_missingRequest(String name, MediaType request) { - return li().withText(String.format("Deleted body: '%s'", name)); + return li().withText(I18n.getMessage("deleted.body", name)); } private LiTag li_changedRequest(String name, ChangedMediaType request) { - LiTag li = li().withText(String.format("Changed body: '%s'", name)); + LiTag li = li().withText(I18n.getMessage("changed.body", name)); ChangedSchema schema = request.getSchema(); if (schema != null) { li.with(div_changedSchema(schema)); @@ -362,7 +372,10 @@ private LiTag li_changedRequest(String name, ChangedMediaType request) { private DivTag div_changedSchema(ChangedSchema schema) { DivTag div = div(); - div.with(h3("Schema" + (schema.isIncompatible() ? " incompatible" : ""))); + div.with( + h3( + I18n.getMessage("schema") + + (schema.isIncompatible() ? " " + I18n.getMessage("schema.incompatible") : ""))); return div; } @@ -374,12 +387,24 @@ private void allChanges( final ContainerTag output, String propName, final ChangedSchema schema) { String prefix = propName.isEmpty() ? "" : propName + "."; properties( - output, prefix, "Missing property", schema.getMissingProperties(), schema.getContext()); + output, + prefix, + I18n.getMessage("missing.property"), + schema.getMissingProperties(), + schema.getContext()); properties( - output, prefix, "Added property", schema.getIncreasedProperties(), schema.getContext()); + output, + prefix, + I18n.getMessage("added.property"), + schema.getIncreasedProperties(), + schema.getContext()); propertiesChanged( - output, prefix, "Changed property", schema.getChangedProperties(), schema.getContext()); + output, + prefix, + I18n.getMessage("changed.property"), + schema.getChangedProperties(), + schema.getContext()); if (schema.getItems() != null) { itemsAllChanges(output, propName, schema.getItems()); } @@ -399,11 +424,15 @@ private void incompatibilities( } if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) { String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); - property(output, propName, "Changed property type", type); + property(output, propName, I18n.getMessage("changed.property.type"), type); } String prefix = propName.isEmpty() ? "" : propName + "."; properties( - output, prefix, "Missing property", schema.getMissingProperties(), schema.getContext()); + output, + prefix, + I18n.getMessage("missing.property"), + schema.getMissingProperties(), + schema.getContext()); schema .getChangedProperties() .forEach((name, property) -> incompatibilities(output, prefix + name, property)); @@ -511,7 +540,14 @@ private UlTag ul_param(ChangedParameters changedParameters) { } private LiTag li_addParam(Parameter param) { - return li().withText("Add " + param.getName() + " in " + param.getIn()) + return li().withText( + I18n.getMessage("action.add") + + " " + + param.getName() + + " " + + I18n.getMessage("in") + + " " + + param.getIn()) .with( span(null == param.getDescription() ? "" : ("//" + param.getDescription())) .withClass(COMMENT)); @@ -519,9 +555,9 @@ private LiTag li_addParam(Parameter param) { private LiTag li_missingParam(Parameter param) { return li().withClass(MISSING) - .with(span("Delete")) + .with(span(I18n.getMessage("action.delete"))) .with(del(param.getName())) - .with(span("in ").withText(param.getIn())) + .with(span(I18n.getMessage("in") + " ").withText(param.getIn())) .with( span(null == param.getDescription() ? "" : ("//" + param.getDescription())) .withClass(COMMENT)); @@ -529,9 +565,9 @@ private LiTag li_missingParam(Parameter param) { private LiTag li_deprecatedParam(ChangedParameter param) { return li().withClass(MISSING) - .with(span("Deprecated")) + .with(span(I18n.getMessage("action.deprecated"))) .with(del(param.getName())) - .with(span("in ").withText(param.getIn())) + .with(span(I18n.getMessage("in") + " ").withText(param.getIn())) .with( span(null == param.getNewParameter().getDescription() ? "" @@ -550,18 +586,22 @@ private LiTag li_changedParam(ChangedParameter changeParam) { .orElse(false); Parameter rightParam = changeParam.getNewParameter(); Parameter leftParam = changeParam.getOldParameter(); - LiTag li = li().withText(changeParam.getName() + " in " + changeParam.getIn()); + LiTag li = + li().withText( + changeParam.getName() + " " + I18n.getMessage("in") + " " + changeParam.getIn()); if (changeRequired) { li.withText( - " change into " + " " + + I18n.getMessage("change.into") + + " " + (rightParam.getRequired() != null && rightParam.getRequired() - ? "required" - : "not required")); + ? I18n.getMessage("required") + : I18n.getMessage("not.required"))); } if (changeDescription) { - li.withText(" Notes ") + li.withText(" " + I18n.getMessage("notes") + " ") .with(del(leftParam.getDescription()).withClass(COMMENT)) - .withText(" change into ") + .withText(" " + I18n.getMessage("change.into") + " ") .with(span(rightParam.getDescription()).withClass(COMMENT)); } return li; @@ -571,9 +611,12 @@ private UlTag ul_operation_id(ChangedOperationId changedOperationId) { return ul().withClass("change") .with( li().withText( - "Changed " + I18n.getMessage("action.changed") + + " " + Optional.ofNullable(changedOperationId.getLeft()).orElse("") - + " to " + + " " + + I18n.getMessage("to") + + " " + Optional.ofNullable(changedOperationId.getRight()).orElse(""))); } } diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/I18n.java b/core/src/main/java/org/openapitools/openapidiff/core/output/I18n.java new file mode 100644 index 000000000..342c4dc2c --- /dev/null +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/I18n.java @@ -0,0 +1,72 @@ +package org.openapitools.openapidiff.core.output; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.PropertyResourceBundle; +import java.util.ResourceBundle; + +public final class I18n { + + private static final String BUNDLE_NAME = "i18n.messages"; + private static volatile Locale currentLocale = Locale.ENGLISH; + private static volatile ResourceBundle bundle = loadBundle(currentLocale); + + private I18n() {} + + public static synchronized void setLocale(Locale locale) { + bundle = loadBundle(locale); + currentLocale = locale; + } + + public static Locale getLocale() { + return currentLocale; + } + + public static String getMessage(String key) { + return bundle.getString(key); + } + + public static String getMessage(String key, Object... args) { + return String.format(bundle.getString(key), args); + } + + public static Locale parseLocale(String lang) { + switch (lang.toLowerCase().replace("_", "-")) { + case "zh-hant": + case "zh-tw": + return Locale.TRADITIONAL_CHINESE; + case "zh-hans": + case "zh-cn": + return Locale.SIMPLIFIED_CHINESE; + case "en": + default: + return Locale.ENGLISH; + } + } + + private static ResourceBundle loadBundle(Locale locale) { + return ResourceBundle.getBundle(BUNDLE_NAME, locale, new UTF8Control()); + } + + private static class UTF8Control extends ResourceBundle.Control { + @Override + public ResourceBundle newBundle( + String baseName, Locale locale, String format, ClassLoader loader, boolean reload) + throws IllegalAccessException, InstantiationException, IOException { + String bundleName = toBundleName(baseName, locale); + String resourceName = toResourceName(bundleName, "properties"); + InputStream stream = loader.getResourceAsStream(resourceName); + if (stream != null) { + try { + return new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8)); + } finally { + stream.close(); + } + } + return super.newBundle(baseName, locale, format, loader, reload); + } + } +} diff --git a/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java b/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java index 531fd3809..a64d3725c 100644 --- a/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java +++ b/core/src/main/java/org/openapitools/openapidiff/core/output/MarkdownRender.java @@ -57,11 +57,12 @@ public void render(ChangedOpenApi diff, OutputStreamWriter outputStreamWriter) { diff.getNewSpecOpenApi().getInfo().getTitle(), diff.getNewSpecOpenApi().getInfo().getVersion(), outputStreamWriter); - listEndpoints("What's New", diff.getNewEndpoints(), outputStreamWriter); - listEndpoints("What's Deleted", diff.getMissingEndpoints(), outputStreamWriter); - listEndpoints("What's Deprecated", diff.getDeprecatedEndpoints(), outputStreamWriter); + listEndpoints(I18n.getMessage("whats.new"), diff.getNewEndpoints(), outputStreamWriter); + listEndpoints(I18n.getMessage("whats.deleted"), diff.getMissingEndpoints(), outputStreamWriter); + listEndpoints( + I18n.getMessage("whats.deprecated"), diff.getDeprecatedEndpoints(), outputStreamWriter); listEndpoints(diff.getChangedOperations(), outputStreamWriter); - changeSummary("Result", diff, outputStreamWriter); + changeSummary(I18n.getMessage("result"), diff, outputStreamWriter); try { outputStreamWriter.close(); } catch (IOException e) { @@ -82,11 +83,11 @@ protected void changeSummary( safelyAppend(outputStreamWriter, sectionTitle(title)); if (diff.isUnchanged()) { - safelyAppend(outputStreamWriter, "No differences. Specifications are equivalent"); + safelyAppend(outputStreamWriter, I18n.getMessage("no.differences")); } else if (diff.isCompatible()) { - safelyAppend(outputStreamWriter, "API changes are backward compatible"); + safelyAppend(outputStreamWriter, I18n.getMessage("api.changes.backward.compatible")); } else { - safelyAppend(outputStreamWriter, "API changes broke backward compatibility"); + safelyAppend(outputStreamWriter, I18n.getMessage("api.changes.broke.compatibility")); } safelyAppend(outputStreamWriter, "\n\n"); @@ -124,7 +125,7 @@ protected void listEndpoints( if (null == changedOperations || changedOperations.isEmpty()) { return; } - safelyAppend(outputStreamWriter, sectionTitle("What's Changed")); + safelyAppend(outputStreamWriter, sectionTitle(I18n.getMessage("whats.changed"))); changedOperations.forEach( operation -> { safelyAppend( @@ -134,22 +135,23 @@ protected void listEndpoints( operation.getPathUrl(), operation.getSummary())); if (result(operation.getOperationId()).isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Operation ID:")); + safelyAppend(outputStreamWriter, titleH5(I18n.getMessage("operation.id") + ":")); safelyAppend(outputStreamWriter, operationId(operation.getOperationId())); } if (result(operation.getParameters()).isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Parameters:")); + safelyAppend(outputStreamWriter, titleH5(I18n.getMessage("parameters") + ":")); safelyAppend(outputStreamWriter, parameters(operation.getParameters())); } if (operation.resultRequestBody().isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Request:")); + safelyAppend(outputStreamWriter, titleH5(I18n.getMessage("request") + ":")); safelyAppend( outputStreamWriter, - metadata("Description", operation.getRequestBody().getDescription())); + metadata( + I18n.getMessage("description"), operation.getRequestBody().getDescription())); safelyAppend(outputStreamWriter, bodyContent(operation.getRequestBody().getContent())); } if (operation.resultApiResponses().isDifferent()) { - safelyAppend(outputStreamWriter, titleH5("Return Type:")); + safelyAppend(outputStreamWriter, titleH5(I18n.getMessage("return.type") + ":")); safelyAppend(outputStreamWriter, responses(operation.getApiResponses())); } }); @@ -157,8 +159,8 @@ protected void listEndpoints( protected String responses(ChangedApiResponse changedApiResponse) { StringBuilder sb = new StringBuilder("\n"); - sb.append(listResponse("New response", changedApiResponse.getIncreased())); - sb.append(listResponse("Deleted response", changedApiResponse.getMissing())); + sb.append(listResponse(I18n.getMessage("new.response"), changedApiResponse.getIncreased())); + sb.append(listResponse(I18n.getMessage("deleted.response"), changedApiResponse.getMissing())); changedApiResponse.getChanged().entrySet().stream() .map(e -> this.itemResponse(e.getKey(), e.getValue())) .forEach(sb::append); @@ -181,7 +183,7 @@ protected String itemResponse(String code, ChangedResponse response) { StringBuilder sb = new StringBuilder(); sb.append( this.itemResponse( - "Changed response", + I18n.getMessage("changed.response"), code, null == response.getNewApiResponse() ? "" @@ -207,8 +209,8 @@ protected String itemResponse(String title, String code, String description) { protected String headers(ChangedHeaders headers) { StringBuilder sb = new StringBuilder(); if (headers != null) { - sb.append(listHeader("New header", headers.getIncreased())) - .append(listHeader("Deleted header", headers.getMissing())); + sb.append(listHeader(I18n.getMessage("new.header"), headers.getIncreased())) + .append(listHeader(I18n.getMessage("deleted.header"), headers.getMissing())); headers.getChanged().entrySet().stream() .map(e -> this.itemHeader(e.getKey(), e.getValue())) .forEach(sb::append); @@ -230,7 +232,7 @@ protected String itemHeader(String title, String name, Header header) { protected String itemHeader(String code, ChangedHeader header) { return this.itemHeader( - "Changed header", + I18n.getMessage("changed.header"), code, null == header.getNewHeader() ? "" : header.getNewHeader().getDescription()); } @@ -244,8 +246,10 @@ protected String bodyContent(String prefix, ChangedContent changedContent) { return ""; } StringBuilder sb = new StringBuilder("\n"); - sb.append(listContent(prefix, "New content type", changedContent.getIncreased())); - sb.append(listContent(prefix, "Deleted content type", changedContent.getMissing())); + sb.append( + listContent(prefix, I18n.getMessage("new.content.type"), changedContent.getIncreased())); + sb.append( + listContent(prefix, I18n.getMessage("deleted.content.type"), changedContent.getMissing())); final int deepness; if (StringUtils.isNotBlank(prefix)) { deepness = 1; @@ -279,7 +283,7 @@ protected String itemContent(String title, String mediaType, MediaType content) } protected String itemContent(int deepness, String mediaType, ChangedMediaType content) { - String result = itemContent("Changed content type", mediaType); + String result = itemContent(I18n.getMessage("changed.content.type"), mediaType); if (content.getSchema() != null) { result += schema(deepness, content.getSchema()); } @@ -296,18 +300,31 @@ protected String oneOfSchema(int deepness, ChangedOneOfSchema schema, String dis .getMissing() .keySet() .forEach( - key -> sb.append(format("%sDeleted '%s' %s\n", indent(deepness), key, discriminator))); + key -> + sb.append( + format( + "%s%s '%s' %s\n", + indent(deepness), I18n.getMessage("action.deleted"), key, discriminator))); schema .getIncreased() .forEach( (key, sub) -> - sb.append(format("%sAdded '%s' %s:\n", indent(deepness), key, discriminator)) + sb.append( + format( + "%s%s '%s' %s:\n", + indent(deepness), I18n.getMessage("action.added"), key, discriminator)) .append(schema(deepness, sub, schema.getContext()))); schema .getChanged() .forEach( (key, sub) -> - sb.append(format("%sUpdated `%s` %s:\n", indent(deepness), key, discriminator)) + sb.append( + format( + "%s%s `%s` %s:\n", + indent(deepness), + I18n.getMessage("action.updated"), + key, + discriminator)) .append(schema(deepness, sub))); return sb.toString(); } @@ -335,24 +352,32 @@ protected String schema(int deepness, ChangedSchema schema) { sb.append(oneOfSchema(deepness, schema.getOneOfSchema(), discriminator)); } if (schema.getRequired() != null) { - sb.append(required(deepness, "New required properties", schema.getRequired().getIncreased())); - sb.append(required(deepness, "New optional properties", schema.getRequired().getMissing())); + sb.append( + required( + deepness, + I18n.getMessage("new.required.properties"), + schema.getRequired().getIncreased())); + sb.append( + required( + deepness, + I18n.getMessage("new.optional.properties"), + schema.getRequired().getMissing())); } if (schema.getItems() != null) { sb.append(items(deepness, schema.getItems())); } - sb.append(listDiff(deepness, "enum", schema.getEnumeration())); + sb.append(listDiff(deepness, I18n.getMessage("enum"), schema.getEnumeration())); sb.append( properties( deepness, - "Added property", + I18n.getMessage("added.property"), schema.getIncreasedProperties(), true, schema.getContext())); sb.append( properties( deepness, - "Deleted property", + I18n.getMessage("deleted.property"), schema.getMissingProperties(), false, schema.getContext())); @@ -372,7 +397,7 @@ protected String schema(int deepness, ComposedSchema schema, DiffContext context } if (schema.getOneOf() != null) { LOGGER.debug("One of schema"); - sb.append(format("%sOne of:\n\n", indent(deepness))); + sb.append(format("%s%s:\n\n", indent(deepness), I18n.getMessage("one.of"))); schema.getOneOf().stream() .map(this::resolve) .forEach(composedChild -> sb.append(schema(deepness + 1, composedChild, context))); @@ -384,8 +409,9 @@ protected String schema(int deepness, Schema schema, DiffContext context) { if (handledSchemas.contains(schema)) return ""; handledSchemas.add(schema); StringBuilder sb = new StringBuilder(); - sb.append(listItem(deepness, "Enum", schema.getEnum())); - sb.append(properties(deepness, "Property", schema.getProperties(), true, context)); + sb.append(listItem(deepness, I18n.getMessage("enum.label"), schema.getEnum())); + sb.append( + properties(deepness, I18n.getMessage("property"), schema.getProperties(), true, context)); if (schema instanceof ComposedSchema) { sb.append(schema(deepness, (ComposedSchema) schema, context)); } else if (schema instanceof ArraySchema) { @@ -400,13 +426,18 @@ protected String items(int deepness, ChangedSchema schema) { if (schema.isChangedType()) { type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); } - sb.append(items(deepness, "Changed items", type, schema.getNewSchema().getDescription())); + sb.append( + items( + deepness, + I18n.getMessage("changed.items"), + type, + schema.getNewSchema().getDescription())); sb.append(schema(deepness, schema)); return sb.toString(); } protected String items(int deepness, Schema schema, DiffContext context) { - return items(deepness, "Items", type(schema), schema.getDescription()) + return items(deepness, I18n.getMessage("items"), type(schema), schema.getDescription()) + schema(deepness, schema, context); } @@ -450,7 +481,12 @@ protected String property(int deepness, String name, ChangedSchema schema) { type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); } sb.append( - property(deepness, "Changed property", name, type, schema.getNewSchema().getDescription())); + property( + deepness, + I18n.getMessage("changed.property"), + name, + type, + schema.getNewSchema().getDescription())); sb.append(schema(++deepness, schema)); return sb.toString(); } @@ -470,14 +506,16 @@ protected String listDiff(int deepness, String name, ChangedList listDiff) { if (listDiff == null) { return ""; } - return listItem(deepness, "Added " + name, listDiff.getIncreased()) - + listItem(deepness, "Removed " + name, listDiff.getMissing()); + return listItem(deepness, I18n.getMessage("action.added") + " " + name, listDiff.getIncreased()) + + listItem(deepness, I18n.getMessage("action.removed") + " " + name, listDiff.getMissing()); } protected String listItem(int deepness, String name, List list) { StringBuilder sb = new StringBuilder(); if (list != null && !list.isEmpty()) { - sb.append(format("%s%s value%s:\n\n", indent(deepness), name, list.size() > 1 ? "s" : "")); + String valueWord = + list.size() > 1 ? I18n.getMessage("value.plural") : I18n.getMessage("value.singular"); + sb.append(format("%s%s %s:\n\n", indent(deepness), name, valueWord)); list.forEach(p -> sb.append(format("%s* `%s`\n", indent(deepness), p))); } return sb.toString(); @@ -486,8 +524,8 @@ protected String listItem(int deepness, String name, List list) { protected String parameters(ChangedParameters changedParameters) { List changed = changedParameters.getChanged(); StringBuilder sb = new StringBuilder("\n"); - sb.append(listParameter("Added", changedParameters.getIncreased())) - .append(listParameter("Deleted", changedParameters.getMissing())); + sb.append(listParameter(I18n.getMessage("action.added"), changedParameters.getIncreased())) + .append(listParameter(I18n.getMessage("action.deleted"), changedParameters.getMissing())); changed.stream().map(this::itemParameter).forEach(sb::append); return sb.toString(); } @@ -506,7 +544,9 @@ protected String itemParameter(String title, Parameter parameter) { protected String itemParameter(String title, String name, String in, String description) { return format("%s: ", title) + code(name) - + " in " + + " " + + I18n.getMessage("in") + + " " + code(in) + '\n' + metadata(description) @@ -517,10 +557,16 @@ protected String itemParameter(ChangedParameter param) { Parameter rightParam = param.getNewParameter(); if (param.isDeprecated()) { return itemParameter( - "Deprecated", rightParam.getName(), rightParam.getIn(), rightParam.getDescription()); + I18n.getMessage("action.deprecated"), + rightParam.getName(), + rightParam.getIn(), + rightParam.getDescription()); } return itemParameter( - "Changed", rightParam.getName(), rightParam.getIn(), rightParam.getDescription()); + I18n.getMessage("action.changed"), + rightParam.getName(), + rightParam.getIn(), + rightParam.getDescription()); } protected String code(String string) { @@ -537,9 +583,11 @@ protected String metadata(String beginning, String name, ChangedMetadata changed } if (!isUnchanged(changedMetadata) && showChangedMetadata) { return format( - "Changed %s:\n%s\nto:\n%s\n\n", + "%s %s:\n%s\n%s:\n%s\n\n", + I18n.getMessage("action.changed"), name, metadata(beginning, changedMetadata.getLeft()), + I18n.getMessage("to"), metadata(beginning, changedMetadata.getRight())); } else { return metadata(beginning, name, changedMetadata.getRight()); @@ -548,7 +596,11 @@ protected String metadata(String beginning, String name, ChangedMetadata changed protected String operationId(ChangedOperationId operationId) { return String.format( - "\nChanged: %s to %s\n\n", code(operationId.getLeft()), code(operationId.getRight())); + "\n%s: %s %s %s\n\n", + I18n.getMessage("action.changed"), + code(operationId.getLeft()), + I18n.getMessage("to"), + code(operationId.getRight())); } protected String metadata(String metadata) { diff --git a/core/src/main/resources/i18n/messages.properties b/core/src/main/resources/i18n/messages.properties new file mode 100644 index 000000000..713c36ad5 --- /dev/null +++ b/core/src/main/resources/i18n/messages.properties @@ -0,0 +1,107 @@ +# Section Headers +whats.new=What's New +whats.deleted=What's Deleted +whats.deprecated=What's Deprecated +whats.changed=What's Changed +result=Result + +# Page Titles +api.change.log=Api Change Log + +# Labels +operation.id=Operation ID +parameter=Parameter +parameters=Parameters +request=Request +return.type=Return Type +response=Response +schema=Schema +media.types=Media types +security.requirements=Security Requirements +description=Description + +# Actions +action.add=Add +action.added=Added +action.delete=Delete +action.deleted=Deleted +action.changed=Changed +action.deprecated=Deprecated +action.updated=Updated +action.removed=Removed + +# Property Labels +added.property=Added property +deleted.property=Deleted property +changed.property=Changed property +changed.property.type=Changed property type +missing.property=Missing property +property=Property + +# Content Type +new.content.type=New content type +deleted.content.type=Deleted content type +changed.content.type=Changed content type + +# Body +new.body=New body: '%s' +deleted.body=Deleted body: '%s' +changed.body=Changed body: '%s' + +# Response +new.response=New response +deleted.response=Deleted response +changed.response=Changed response +new.response.code=New response : [%s] +deleted.response.code=Deleted response : [%s] +changed.response.code=Changed response : [%s] + +# Header +new.header=New header +deleted.header=Deleted header +changed.header=Changed header + +# Security +new.security.requirement=New security requirement : +deleted.security.requirement=Deleted security requirement : +changed.security.requirement=Changed security requirement : + +# Schema +backward.compatible=Backward compatible +broken.compatibility=Broken compatibility +schema.incompatible=incompatible +new.required.properties=New required properties +new.optional.properties=New optional properties +items=Items +changed.items=Changed items +enum=enum +enum.label=Enum +one.of=One of + +# Compatibility Messages +api.changes.backward.compatible=API changes are backward compatible +api.changes.broke.compatibility=API changes broke backward compatibility +no.differences=No differences. Specifications are equivalent + +# Asciidoc specific +note.no.differences=NOTE: No differences. Specifications are equivalent +note.backward.compatible=NOTE: API changes are backward compatible +warning.broke.compatibility=WARNING: API changes broke backward compatibility + +# Misc +in=in +to=to +change.into=change into +required=required +not.required=not required +notes=Notes +value.singular=value +value.plural=values + +# CLI +cli.version.prefix=openapi-diff version: +cli.parsing.failed=Parsing failed. Reason: +cli.unexpected.exception=Unexpected exception. Reason: +cli.missing.arguments=Missing arguments +cli.invalid.log.level=Invalid log level. Expected: [TRACE, DEBUG, INFO, WARN, ERROR, OFF]. Given: %s +cli.config.prop.unexpected.format=--config-prop unexpected format: diff --git a/core/src/main/resources/i18n/messages_zh_CN.properties b/core/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 000000000..bea5a22d8 --- /dev/null +++ b/core/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,107 @@ +# 章节标题 +whats.new=新增项目 +whats.deleted=删除项目 +whats.deprecated=弃用项目 +whats.changed=变更项目 +result=结果 + +# 页面标题 +api.change.log=API 变更日志 + +# 标签 +operation.id=操作 ID +parameter=参数 +parameters=参数 +request=请求 +return.type=响应类型 +response=响应 +schema=结构描述 +media.types=媒体类型 +security.requirements=安全性需求 +description=描述 + +# 操作 +action.add=新增 +action.added=新增 +action.delete=删除 +action.deleted=已删除 +action.changed=变更 +action.deprecated=已弃用 +action.updated=已更新 +action.removed=移除 + +# 属性标签 +added.property=新增属性 +deleted.property=删除属性 +changed.property=变更属性 +changed.property.type=变更属性类型 +missing.property=缺少属性 +property=属性 + +# 内容类型 +new.content.type=新增内容类型 +deleted.content.type=删除内容类型 +changed.content.type=变更内容类型 + +# 主体 +new.body=新增主体:'%s' +deleted.body=删除主体:'%s' +changed.body=变更主体:'%s' + +# 响应 +new.response=新增响应 +deleted.response=删除响应 +changed.response=变更响应 +new.response.code=新增响应:[%s] +deleted.response.code=删除响应:[%s] +changed.response.code=变更响应:[%s] + +# 标头 +new.header=新增标头 +deleted.header=删除标头 +changed.header=变更标头 + +# 安全性 +new.security.requirement=新增安全性需求: +deleted.security.requirement=删除安全性需求: +changed.security.requirement=变更安全性需求: + +# 结构描述 +backward.compatible=向后兼容 +broken.compatibility=兼容性中断 +schema.incompatible=不兼容 +new.required.properties=新增必要属性 +new.optional.properties=新增可选属性 +items=项目 +changed.items=变更项目 +enum=枚举 +enum.label=枚举 +one.of=其中之一 + +# 兼容性消息 +api.changes.backward.compatible=API 变更具向后兼容性 +api.changes.broke.compatibility=API 变更破坏了向后兼容性 +no.differences=无差异,规格相同 + +# Asciidoc 专用 +note.no.differences=NOTE: 无差异,规格相同 +note.backward.compatible=NOTE: API 变更具向后兼容性 +warning.broke.compatibility=WARNING: API 变更破坏了向后兼容性 + +# 其他 +in=于 +to=为 +change.into=变更为 +required=必要 +not.required=非必要 +notes=备注 +value.singular=值 +value.plural=值 + +# CLI +cli.version.prefix=openapi-diff 版本: +cli.parsing.failed=解析失败。原因: +cli.unexpected.exception=发生非预期的异常。原因: +cli.missing.arguments=缺少参数 +cli.invalid.log.level=无效的日志等级。预期:[TRACE, DEBUG, INFO, WARN, ERROR, OFF]。输入:%s +cli.config.prop.unexpected.format=--config-prop 格式不正确: diff --git a/core/src/main/resources/i18n/messages_zh_TW.properties b/core/src/main/resources/i18n/messages_zh_TW.properties new file mode 100644 index 000000000..d2541a0c7 --- /dev/null +++ b/core/src/main/resources/i18n/messages_zh_TW.properties @@ -0,0 +1,107 @@ +# 章節標題 +whats.new=新增項目 +whats.deleted=刪除項目 +whats.deprecated=棄用項目 +whats.changed=變更項目 +result=結果 + +# 頁面標題 +api.change.log=API 變更日誌 + +# 標籤 +operation.id=操作 ID +parameter=參數 +parameters=參數 +request=請求 +return.type=回應類型 +response=回應 +schema=結構描述 +media.types=媒體類型 +security.requirements=安全性需求 +description=描述 + +# 操作 +action.add=新增 +action.added=新增 +action.delete=刪除 +action.deleted=已刪除 +action.changed=變更 +action.deprecated=已棄用 +action.updated=已更新 +action.removed=移除 + +# 屬性標籤 +added.property=新增屬性 +deleted.property=刪除屬性 +changed.property=變更屬性 +changed.property.type=變更屬性類型 +missing.property=缺少屬性 +property=屬性 + +# 內容類型 +new.content.type=新增內容類型 +deleted.content.type=刪除內容類型 +changed.content.type=變更內容類型 + +# 主體 +new.body=新增主體:'%s' +deleted.body=刪除主體:'%s' +changed.body=變更主體:'%s' + +# 回應 +new.response=新增回應 +deleted.response=刪除回應 +changed.response=變更回應 +new.response.code=新增回應:[%s] +deleted.response.code=刪除回應:[%s] +changed.response.code=變更回應:[%s] + +# 標頭 +new.header=新增標頭 +deleted.header=刪除標頭 +changed.header=變更標頭 + +# 安全性 +new.security.requirement=新增安全性需求: +deleted.security.requirement=刪除安全性需求: +changed.security.requirement=變更安全性需求: + +# 結構描述 +backward.compatible=向後相容 +broken.compatibility=相容性中斷 +schema.incompatible=不相容 +new.required.properties=新增必要屬性 +new.optional.properties=新增選用屬性 +items=項目 +changed.items=變更項目 +enum=列舉 +enum.label=列舉 +one.of=其中之一 + +# 相容性訊息 +api.changes.backward.compatible=API 變更具向後相容性 +api.changes.broke.compatibility=API 變更破壞了向後相容性 +no.differences=無差異,規格相同 + +# Asciidoc 專用 +note.no.differences=NOTE: 無差異,規格相同 +note.backward.compatible=NOTE: API 變更具向後相容性 +warning.broke.compatibility=WARNING: API 變更破壞了向後相容性 + +# 其他 +in=於 +to=為 +change.into=變更為 +required=必要 +not.required=非必要 +notes=備註 +value.singular=值 +value.plural=值 + +# CLI +cli.version.prefix=openapi-diff 版本: +cli.parsing.failed=解析失敗。原因: +cli.unexpected.exception=發生非預期的例外。原因: +cli.missing.arguments=缺少參數 +cli.invalid.log.level=無效的日誌等級。預期:[TRACE, DEBUG, INFO, WARN, ERROR, OFF]。輸入:%s +cli.config.prop.unexpected.format=--config-prop 格式不正確: