I18nTimeDataGenerator.java

/* ========================================================================
 * PlantUML : a free UML diagram generator
 * ========================================================================
 *
 * (C) Copyright 2009-2024, Arnaud Roques
 *
 * Project Info:  https://plantuml.com
 * 
 * If you like this project or if you find it useful, you can support us at:
 * 
 * https://plantuml.com/patreon (only 1$ per month!)
 * https://plantuml.com/paypal
 * 
 * This file is part of PlantUML.
 *
 * PlantUML is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PlantUML distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 *
 * Original Author:  Arnaud Roques
 * 
 *
 */
package net.sourceforge.plantuml.utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.format.TextStyle;
import java.util.Locale;

/**
 * Generates {@code I18nTimeData.java} by extracting short localized names for
 * {@link DayOfWeek} and {@link Month} from the running JVM.
 * <p>
 * Run this class on a standard JVM (NOT under TeaVM) from the project root. It
 * overwrites {@code src/main/java/net/sourceforge/plantuml/utils/I18nTimeData.java}
 * in place:
 *
 * <pre>
 * java net.sourceforge.plantuml.utils.I18nTimeDataGenerator
 * </pre>
 *
 * The generated class exposes {@code shortName(DayOfWeek, Locale)} and
 * {@code shortName(Month, Locale)} implementations that do not rely on
 * {@code ResourceBundle} or {@code DateTimeTextProvider}, so they are safe to
 * use in a TeaVM-compiled context.
 * <p>
 * back to the English short form.
 */
public class I18nTimeDataGenerator {

	// Languages supported by the generated I18nTimeData.
	// Order only affects readability of the generated code; dispatch is done
	// on Locale.getLanguage() and each branch is independent.
	private static final String[] LANGUAGES = { "en", "de", "es", "fr", "ja", "ko", "ru", "zh" };

	public static void main(String[] args) throws Exception {
		// Write I18nData.java next to this generator's source file.
		// The path is resolved relative to the current working directory, which
		// is expected to be the project root (same convention as other PlantUML
		// code-generation utilities).
		final File output = new File("src/main/java/net/sourceforge/plantuml/utils/I18nTimeData.java");
		final File parent = output.getParentFile();
		if (parent != null && !parent.isDirectory())
			throw new IOException("Target directory does not exist: " + parent);

		try (PrintStream out = new PrintStream(new FileOutputStream(output), false, StandardCharsets.UTF_8.name())) {
			emitHeader(out);
			emitDayOfWeekMethod(out);
			out.println();
			emitMonthMethod(out);
			out.println();
			emitMonthLongNameMethod(out);
			emitFooter(out);
		}

		System.out.println("Generated " + output.getAbsolutePath());
	}

	private static void emitHeader(PrintStream out) {
		out.println("package net.sourceforge.plantuml.utils;");
		out.println();
		out.println("import java.time.DayOfWeek;");
		out.println("import java.time.Month;");
		out.println("import java.util.Locale;");
		out.println();
		out.println("// Generated \u2014 do not edit");
		out.println("// Build by I18nDataTimeGenerator");
		out.println("public class I18nTimeData {");
		out.println();
	}

	private static void emitFooter(PrintStream out) {
		out.println("}");
	}

	private static void emitDayOfWeekMethod(PrintStream out) {
		out.println("\tpublic static String shortName(DayOfWeek dayOfWeek, Locale locale) {");
		out.println("\t\tfinal String lang = locale.getLanguage();");
		out.println("\t\tswitch (lang) {");
		for (String lang : LANGUAGES) {
			if ("en".equals(lang))
				continue; // english is the fallback
			final Locale loc = Locale.forLanguageTag(lang);
			out.println("\t\tcase \"" + lang + "\":");
			out.println("\t\t\tswitch (dayOfWeek) {");
			for (DayOfWeek d : DayOfWeek.values()) {
				final String shortName = dayShortName(d, loc);
				out.println("\t\t\tcase " + d.name() + ": return \"" + escape(shortName) + "\";");
			}
			out.println("\t\t\t}");
			out.println("\t\t\tbreak;");
		}
		out.println("\t\t}");
		// English fallback (also used when locale is unsupported)
		out.println("\t\t// Fallback: English short form (first two letters of enum name)");
		out.println("\t\tswitch (dayOfWeek) {");
		for (DayOfWeek d : DayOfWeek.values()) {
			final String shortName = dayShortName(d, Locale.ENGLISH);
			out.println("\t\tcase " + d.name() + ": return \"" + escape(shortName) + "\";");
		}
		out.println("\t\t}");
		out.println("\t\tthrow new IllegalArgumentException();");
		out.println("\t}");
	}

	private static void emitMonthMethod(PrintStream out) {
		out.println("\tpublic static String shortName(Month month, Locale locale) {");
		out.println("\t\tfinal String lang = locale.getLanguage();");
		out.println("\t\tswitch (lang) {");
		for (String lang : LANGUAGES) {
			if ("en".equals(lang))
				continue;
			final Locale loc = Locale.forLanguageTag(lang);
			out.println("\t\tcase \"" + lang + "\":");
			out.println("\t\t\tswitch (month) {");
			for (Month m : Month.values()) {
				final String shortName = monthShortName(m, loc);
				out.println("\t\t\tcase " + m.name() + ": return \"" + escape(shortName) + "\";");
			}
			out.println("\t\t\t}");
			out.println("\t\t\tbreak;");
		}
		out.println("\t\t}");
		out.println("\t\t// Fallback: English short form (first three letters of enum name)");
		out.println("\t\tswitch (month) {");
		for (Month m : Month.values()) {
			final String shortName = monthShortName(m, Locale.ENGLISH);
			out.println("\t\tcase " + m.name() + ": return \"" + escape(shortName) + "\";");
		}
		out.println("\t\t}");
		out.println("\t\tthrow new IllegalArgumentException();");
		out.println("\t}");
	}

	private static void emitMonthLongNameMethod(PrintStream out) {
		out.println("\tpublic static String longName(Month month, Locale locale) {");
		out.println("\t\tfinal String lang = locale.getLanguage();");
		out.println("\t\tswitch (lang) {");
		for (String lang : LANGUAGES) {
			if ("en".equals(lang))
				continue;
			final Locale loc = Locale.forLanguageTag(lang);
			out.println("\t\tcase \"" + lang + "\":");
			out.println("\t\t\tswitch (month) {");
			for (Month m : Month.values()) {
				final String longName = monthLongName(m, loc);
				out.println("\t\t\tcase " + m.name() + ": return \"" + escape(longName) + "\";");
			}
			out.println("\t\t\t}");
			out.println("\t\t\tbreak;");
		}
		out.println("\t\t}");
		out.println("\t\t// Fallback: English long form");
		out.println("\t\tswitch (month) {");
		for (Month m : Month.values()) {
			final String longName = monthLongName(m, Locale.ENGLISH);
			out.println("\t\tcase " + m.name() + ": return \"" + escape(longName) + "\";");
		}
		out.println("\t\t}");
		out.println("\t\tthrow new IllegalArgumentException();");
		out.println("\t}");
	}

	// ------------------------------------------------------------------
	// Reproduce DayOfWeekUtils.shortName / MonthUtils.shortName semantics
	// ------------------------------------------------------------------

	private static String capitalize(String s) {
		if (s == null || s.isEmpty())
			return s;
		return Character.toUpperCase(s.charAt(0)) + s.toLowerCase().substring(1);
	}

	private static String dayShortName(DayOfWeek d, Locale locale) {
		if (locale == Locale.ENGLISH || "en".equals(locale.getLanguage()))
			return capitalize(d.name().substring(0, 2));
		final String s = d.getDisplayName(TextStyle.SHORT_STANDALONE, locale);
		if (s.length() > 2)
			return s.substring(0, 2);
		return s;
	}

	private static String monthShortName(Month m, Locale locale) {
		if (locale == Locale.ENGLISH || "en".equals(locale.getLanguage()))
			return capitalize(m.name()).substring(0, 3);
		return m.getDisplayName(TextStyle.SHORT_STANDALONE, locale);
	}

	private static String monthLongName(Month m, Locale locale) {
		if (locale == Locale.ENGLISH || "en".equals(locale.getLanguage()))
			return capitalize(m.name());
		return m.getDisplayName(TextStyle.FULL_STANDALONE, locale);
	}

	// Escape only characters that have a special meaning inside a Java string
	// literal. Non-ASCII characters are emitted as-is: the generated file is
	// written in UTF-8, which keeps the source human-readable.
	private static String escape(String s) {
		final StringBuilder sb = new StringBuilder();
		for (int i = 0; i < s.length(); i++) {
			final char c = s.charAt(i);
			switch (c) {
			case '"':
			case '\\':
				sb.append('\\').append(c);
				break;
			case '\n':
				sb.append("\\n");
				break;
			case '\r':
				sb.append("\\r");
				break;
			case '\t':
				sb.append("\\t");
				break;
			default:
				sb.append(c);
				break;
			}
		}
		return sb.toString();
	}

}