diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..b21589c --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,12 @@ +project_id_env: NEOFORGE_CROWDIN_PROJECT_ID +api_token_env: NEOFORGE_CROWDIN_API_TOKEN +base_path: . +base_url: 'https://neoforged.api.crowdin.com' +preserve_hierarchy: 1 +commit_message: '[ci skip]' +pull_request_labels: + - l10n +files: + - source: /src/main/resources/neoforged/installer.xml + translation: /src/main/resources/neoforged/installer_%two_letters_code%.%file_extension% + dest: /messages.%file_extension% diff --git a/src/main/java/net/minecraftforge/installer/SimpleInstaller.java b/src/main/java/net/minecraftforge/installer/SimpleInstaller.java index 33af1db..9348b35 100644 --- a/src/main/java/net/minecraftforge/installer/SimpleInstaller.java +++ b/src/main/java/net/minecraftforge/installer/SimpleInstaller.java @@ -43,6 +43,7 @@ import net.minecraftforge.installer.actions.ProgressCallback; import net.minecraftforge.installer.json.InstallV1; import net.minecraftforge.installer.json.Util; +import net.minecraftforge.installer.ui.InstallerPanel; import net.neoforged.cliutils.progress.ProgressInterceptor; import net.neoforged.cliutils.progress.ProgressManager; import net.neoforged.cliutils.progress.ProgressReporter; @@ -209,7 +210,7 @@ private static OutputStream getLog() throws FileNotFoundException return new BufferedOutputStream(new FileOutputStream(output)); } - static void hookStdOut(ProgressCallback monitor) + public static void hookStdOut(ProgressCallback monitor) { final Pattern endingWhitespace = Pattern.compile("\\r?\\n$"); final OutputStream monitorStream = new OutputStream() { diff --git a/src/main/java/net/minecraftforge/installer/actions/Action.java b/src/main/java/net/minecraftforge/installer/actions/Action.java index c6ace21..05b107a 100644 --- a/src/main/java/net/minecraftforge/installer/actions/Action.java +++ b/src/main/java/net/minecraftforge/installer/actions/Action.java @@ -34,6 +34,7 @@ import net.minecraftforge.installer.json.Version; import net.minecraftforge.installer.json.Version.Library; import net.minecraftforge.installer.json.Version.LibraryDownload; +import net.minecraftforge.installer.ui.TranslatedMessage; public abstract class Action { protected final InstallV1 profile; @@ -57,7 +58,7 @@ protected void error(String message) { public abstract boolean run(File target, Predicate optionals, File installer) throws ActionCanceledException; public abstract TargetValidator getTargetValidator(); - public abstract String getSuccessMessage(); + public abstract TranslatedMessage getSuccessMessage(); public String getSponsorMessage() { return profile.getMirror() != null && profile.getMirror().isAdvertised() ? String.format(SimpleInstaller.headless ? "Data kindly mirrored by %2$s at %1$s" : "Data kindly mirrored by %s", profile.getMirror().getHomepage(), profile.getMirror().getName()) : null; diff --git a/src/main/java/net/minecraftforge/installer/actions/Actions.java b/src/main/java/net/minecraftforge/installer/actions/Actions.java index a6318bf..4896bbe 100644 --- a/src/main/java/net/minecraftforge/installer/actions/Actions.java +++ b/src/main/java/net/minecraftforge/installer/actions/Actions.java @@ -25,9 +25,9 @@ public enum Actions { - CLIENT("Install client", "Install a new profile to the Mojang client launcher", ClientInstall::new, () -> "Successfully installed client into launcher."), - SERVER("Install server", "Create a new modded server installation", ServerInstall::new, () -> "The server installed successfully"), - EXTRACT("Extract", "Extract the contained jar file", ExtractAction::new, () -> "All files successfully extract."); + CLIENT("installer.action.install.client.name", "installer.action.install.client.tooltip", ClientInstall::new, () -> "Successfully installed client into launcher."), + SERVER("installer.action.install.server.name", "installer.action.install.server.tooltip", ServerInstall::new, () -> "The server installed successfully"), + EXTRACT("installer.action.extract.name", "installer.action.extract.tooltip", ExtractAction::new, () -> "All files successfully extract."); private String label; private String tooltip; diff --git a/src/main/java/net/minecraftforge/installer/actions/ClientInstall.java b/src/main/java/net/minecraftforge/installer/actions/ClientInstall.java index c34f25c..ac2f9f2 100644 --- a/src/main/java/net/minecraftforge/installer/actions/ClientInstall.java +++ b/src/main/java/net/minecraftforge/installer/actions/ClientInstall.java @@ -31,6 +31,7 @@ import net.minecraftforge.installer.json.Version.Download; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import net.minecraftforge.installer.ui.TranslatedMessage; public class ClientInstall extends Action { @@ -200,9 +201,10 @@ public TargetValidator getTargetValidator() { } @Override - public String getSuccessMessage() { - if (downloadedCount() > 0) - return String.format("Successfully installed client profile %s for version %s into launcher, and downloaded %d libraries", profile.getProfile(), profile.getVersion(), downloadedCount()); - return String.format("Successfully installed client profile %s for version %s into launcher", profile.getProfile(), profile.getVersion()); + public TranslatedMessage getSuccessMessage() { + if (downloadedCount() > 0) { + return new TranslatedMessage("installer.action.install.client.finished.withlibs", profile.getProfile(), profile.getVersion(), downloadedCount()); + } + return new TranslatedMessage("installer.action.install.client.finished.withoutlibs", profile.getProfile(), profile.getVersion()); } } diff --git a/src/main/java/net/minecraftforge/installer/actions/ExtractAction.java b/src/main/java/net/minecraftforge/installer/actions/ExtractAction.java index 3e131bc..f8bf873 100644 --- a/src/main/java/net/minecraftforge/installer/actions/ExtractAction.java +++ b/src/main/java/net/minecraftforge/installer/actions/ExtractAction.java @@ -24,6 +24,7 @@ import net.minecraftforge.installer.DownloadUtils; import net.minecraftforge.installer.json.Artifact; import net.minecraftforge.installer.json.InstallV1; +import net.minecraftforge.installer.ui.TranslatedMessage; public class ExtractAction extends Action { @@ -88,7 +89,7 @@ public TargetValidator getTargetValidator() { } @Override - public String getSuccessMessage() { - return "Extracted successfully"; + public TranslatedMessage getSuccessMessage() { + return new TranslatedMessage("installer.action.extract.finished"); } } diff --git a/src/main/java/net/minecraftforge/installer/actions/ServerInstall.java b/src/main/java/net/minecraftforge/installer/actions/ServerInstall.java index 7471afc..b0a8175 100644 --- a/src/main/java/net/minecraftforge/installer/actions/ServerInstall.java +++ b/src/main/java/net/minecraftforge/installer/actions/ServerInstall.java @@ -32,6 +32,7 @@ import net.minecraftforge.installer.json.Util; import net.minecraftforge.installer.json.Version; import net.minecraftforge.installer.json.Version.Download; +import net.minecraftforge.installer.ui.TranslatedMessage; public class ServerInstall extends Action { private List grabbed = new ArrayList<>(); @@ -133,9 +134,10 @@ public TargetValidator getTargetValidator() { } @Override - public String getSuccessMessage() { - if (!grabbed.isEmpty()) - return String.format("Successfully downloaded minecraft server, downloaded %d libraries and installed %s", grabbed.size(), profile.getVersion()); - return String.format("Successfully downloaded minecraft server and installed %s", profile.getVersion()); + public TranslatedMessage getSuccessMessage() { + if (grabbed.isEmpty()) { + return new TranslatedMessage("installer.action.install.server.finished.withoutlibs", profile.getVersion()); + } + return new TranslatedMessage("installer.action.install.server.finished.withlibs", grabbed.size(), profile.getVersion()); } } diff --git a/src/main/java/net/minecraftforge/installer/actions/TargetValidator.java b/src/main/java/net/minecraftforge/installer/actions/TargetValidator.java index cb87e9f..f0ae0db 100644 --- a/src/main/java/net/minecraftforge/installer/actions/TargetValidator.java +++ b/src/main/java/net/minecraftforge/installer/actions/TargetValidator.java @@ -18,6 +18,7 @@ */ package net.minecraftforge.installer.actions; +import net.minecraftforge.installer.ui.TranslatedMessage; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -39,24 +40,24 @@ default TargetValidator and(TargetValidator other) { } static TargetValidator isDirectory() { - return target -> target.isDirectory() ? ValidationResult.valid() : ValidationResult.invalid(true, "The specified path needs to be a directory"); + return target -> target.isDirectory() ? ValidationResult.valid() : ValidationResult.invalid(true, "installer.target.error.notdirectory"); } static TargetValidator shouldExist(boolean critical) { - return target -> target.exists() ? ValidationResult.valid() : ValidationResult.invalid(critical, "The specified directory does not exist" + (critical ? "" : "
It will be created")); + return target -> target.exists() ? ValidationResult.valid() : ValidationResult.invalid(critical, critical ? "installer.target.error.directory.doesntexist.critical" : "installer.target.error.directory.doesntexist.create"); } static TargetValidator shouldBeEmpty() { - return target -> Objects.requireNonNull(target.list()).length == 0 ? ValidationResult.valid() : ValidationResult.invalid(false, "There are already files in the target directory"); + return target -> Objects.requireNonNull(target.list()).length == 0 ? ValidationResult.valid() : ValidationResult.invalid(false, "installer.target.error.directory.notempty"); } static TargetValidator isMCInstallationDirectory() { return target -> (new File(target, "launcher_profiles.json").exists() || - new File(target, "launcher_profiles_microsoft_store.json").exists()) ? ValidationResult.valid() : ValidationResult.invalid(true, "The directory is missing a launcher profile. Please run the minecraft launcher first"); + new File(target, "launcher_profiles_microsoft_store.json").exists()) ? ValidationResult.valid() : ValidationResult.invalid(true, "installer.target.error.missingprofile"); } class ValidationResult { - private static final ValidationResult VALID = new ValidationResult(true, false, ""); + private static final ValidationResult VALID = new ValidationResult(true, false, new TranslatedMessage("")); /** * Whether the target directory is valid for installation. @@ -69,9 +70,9 @@ class ValidationResult { /** * A message to display to users if the target is invalid. */ - public final String message; + public final TranslatedMessage message; - private ValidationResult(boolean valid, boolean critical, String message) { + private ValidationResult(boolean valid, boolean critical, TranslatedMessage message) { this.valid = valid; this.critical = critical; this.message = message; @@ -81,8 +82,8 @@ public static ValidationResult valid() { return VALID; } - public static ValidationResult invalid(boolean critical, String message) { - return new ValidationResult(false, critical, message); + public static ValidationResult invalid(boolean critical, String messageKey) { + return new ValidationResult(false, critical, new TranslatedMessage(messageKey)); } public ValidationResult combine(Supplier other) { diff --git a/src/main/java/net/minecraftforge/installer/Images.java b/src/main/java/net/minecraftforge/installer/ui/Images.java similarity index 60% rename from src/main/java/net/minecraftforge/installer/Images.java rename to src/main/java/net/minecraftforge/installer/ui/Images.java index 21d0a52..020000c 100644 --- a/src/main/java/net/minecraftforge/installer/Images.java +++ b/src/main/java/net/minecraftforge/installer/ui/Images.java @@ -1,4 +1,24 @@ -package net.minecraftforge.installer; +/* + * Installer + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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 + */ +package net.minecraftforge.installer.ui; + +import net.minecraftforge.installer.SimpleInstaller; import javax.imageio.ImageIO; import java.awt.Image; diff --git a/src/main/java/net/minecraftforge/installer/InstallerPanel.java b/src/main/java/net/minecraftforge/installer/ui/InstallerPanel.java similarity index 72% rename from src/main/java/net/minecraftforge/installer/InstallerPanel.java rename to src/main/java/net/minecraftforge/installer/ui/InstallerPanel.java index 2a25683..e8ef101 100644 --- a/src/main/java/net/minecraftforge/installer/InstallerPanel.java +++ b/src/main/java/net/minecraftforge/installer/ui/InstallerPanel.java @@ -16,8 +16,9 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package net.minecraftforge.installer; +package net.minecraftforge.installer.ui; +import net.minecraftforge.installer.SimpleInstaller; import net.minecraftforge.installer.actions.Action; import net.minecraftforge.installer.actions.ActionCanceledException; import net.minecraftforge.installer.actions.Actions; @@ -26,42 +27,31 @@ import net.minecraftforge.installer.json.InstallV1; import net.minecraftforge.installer.json.OptionalLibrary; -import javax.swing.AbstractAction; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.ButtonGroup; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JTextField; +import javax.swing.*; import javax.swing.border.LineBorder; -import java.awt.Color; -import java.awt.Desktop; -import java.awt.Dimension; -import java.awt.EventQueue; -import java.awt.Font; -import java.awt.Insets; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; @SuppressWarnings("unused") public class InstallerPanel extends JPanel { + private static final Path INSTALLER_SETTINGS = new File(SimpleInstaller.getMCDir(), ".neoforge_installer.properties").toPath(); + public static final L10nManager TRANSLATIONS = new L10nManager("neoforged/installer", INSTALLER_SETTINGS); + private static final long serialVersionUID = 1L; private File targetDir; private ButtonGroup choiceButtonGroup; @@ -143,7 +133,8 @@ public InstallerPanel(File targetDir, InstallV1 profile, File installer) logoLabel.setAlignmentY(CENTER_ALIGNMENT); logoLabel.setSize(image.getWidth(), image.getHeight()); logoSplash.add(logoLabel); - JLabel tag = new JLabel(profile.getWelcome()); + JLabel tag = new JLabel(); + TRANSLATIONS.translate(tag, TranslationTarget.LABEL_TEXT, profile.getWelcome()); tag.setAlignmentX(CENTER_ALIGNMENT); tag.setAlignmentY(CENTER_ALIGNMENT); logoSplash.add(tag); @@ -151,7 +142,7 @@ public InstallerPanel(File targetDir, InstallV1 profile, File installer) { // The version is a box that has a first label that is non-bold final Box version = Box.createHorizontalBox(); - version.add(new JLabel("Version: ")); + version.add(TRANSLATIONS.label("installer.welcome.version")); tag = new JLabel(profile.getVersion()); // and a bold part which represents the actual version tag.setFont(tag.getFont().deriveFont(Font.BOLD)); @@ -192,11 +183,9 @@ public InstallerPanel(File targetDir, InstallV1 profile, File installer) if (action == Actions.SERVER && profile.hideServer()) continue; if (action == Actions.EXTRACT && profile.hideExtract()) continue; actions.put(action.name(), prog -> action.getAction(profile, prog)); - JRadioButton radioButton = new JRadioButton(); - radioButton.setAction(sba); - radioButton.setText(action.getButtonLabel()); + JRadioButton radioButton = TRANSLATIONS.radioButton(sba, action.getButtonLabel()); + TRANSLATIONS.setTooltip(radioButton, action.getTooltip()); radioButton.setActionCommand(action.name()); - radioButton.setToolTipText(action.getTooltip()); radioButton.setSelected(first); radioButton.setAlignmentX(LEFT_ALIGNMENT); radioButton.setAlignmentY(CENTER_ALIGNMENT); @@ -217,95 +206,19 @@ public InstallerPanel(File targetDir, InstallV1 profile, File installer) choicePanel.setAlignmentY(CENTER_ALIGNMENT); add(choicePanel); - /* - if (VersionInfo.hasOptionals()) - { - optionals = new OptionalListEntry[VersionInfo.getOptionals().size()]; - int x = 0; - for (OptionalLibrary opt : VersionInfo.getOptionals()) - optionals[x++] = new OptionalListEntry(opt); - - final JList list = new JList(optionals); - - list.setCellRenderer(new ListCellRenderer() - { - private JPanel panel = new JPanel(new BorderLayout()); - private JCheckBox check = new JCheckBox(); - private JLabel icon = new JLabel(new ImageIcon(urlIcon)); - { - check.setHorizontalAlignment(SwingConstants.LEFT); - icon.setSize(urlIcon.getWidth(), urlIcon.getHeight()); - panel.add(check, BorderLayout.LINE_START); - panel.add(icon, BorderLayout.LINE_END); - } - - @Override - public Component getListCellRendererComponent(JList list, OptionalListEntry value, int index, boolean isSelected, boolean cellHasFocus) - { - check.setSelected(value.isEnabled()); - check.setText(value.lib.getName()); - icon.setVisible(value.lib.getURL() != null); - return panel; - } - }); - - list.addMouseListener(new MouseAdapter() - { - public void mouseClicked(MouseEvent event) - { - int index = list.locationToIndex(event.getPoint()); - OptionalListEntry entry = list.getModel().getElementAt(index); - - if (entry.lib.getURL() != null && event.getPoint().getX() > list.getWidth() - urlIcon.getWidth()) - openURL(entry.lib.getURL()); - else - entry.setEnabled(!entry.isEnabled()); - list.repaint(list.getCellBounds(index, index)); - } - }); - list.addMouseMotionListener(new MouseMotionListener() - { - public void mouseMoved(MouseEvent event) - { - int index = list.locationToIndex(event.getPoint()); - OptionalListEntry entry = list.getModel().getElementAt(index); - if (entry.lib.getDesc() != null) - { - StringBuilder tt = new StringBuilder(); - tt.append(""); - //tt.append("

").append(index).append(" ").append(entry.lib.getName()).append("

"); - //if (entry.lib.getURL() != null) - // tt.append(" URL: ").append(entry.lib.getURL()).append("
"); - if (entry.lib.getDesc() != null) - tt.append(entry.lib.getDesc()); - tt.append(""); - list.setToolTipText(tt.toString()); - } - else - list.setToolTipText(null); - } - - @Override public void mouseDragged(MouseEvent event) {} - }); - - - add(new JScrollPane(list)); - } - */ - JPanel entryPanel = new JPanel(); entryPanel.setLayout(new BoxLayout(entryPanel,BoxLayout.X_AXIS)); this.targetDir = targetDir; selectedDirText = new JTextField(); selectedDirText.setEditable(false); - selectedDirText.setToolTipText("Path to the Minecraft installation directory"); + TRANSLATIONS.setTooltip(selectedDirText, "installer.welcome.target.tooltip"); selectedDirText.setColumns(30); entryPanel.add(selectedDirText); JButton dirSelect = new JButton(); dirSelect.setAction(new FileSelectAction()); dirSelect.setText("..."); - dirSelect.setToolTipText("Select an alternative Minecraft directory"); + TRANSLATIONS.setTooltip(dirSelect, "installer.welcome.dirselect.tooltip"); entryPanel.add(dirSelect); entryPanel.setAlignmentX(LEFT_ALIGNMENT); @@ -368,7 +281,7 @@ private void updateFilePath() } else { selectedDirText.setForeground(Color.RED); fileEntryPanel.setBorder(new LineBorder(Color.RED)); - infoLabel.setText(""+valid.message+""); + TRANSLATIONS.translate(infoLabel, TranslationTarget.html(TranslationTarget.LABEL_TEXT), valid.message.key, valid.message.arguments); infoLabel.setVisible(true); proceedButton.ifPresent(button -> button.setEnabled(!valid.critical)); } @@ -382,25 +295,41 @@ private void updateFilePath() public void run(ProgressCallback monitor) { - JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); + final JComboBox languageBox = new JComboBox<>(); + + final List known = TRANSLATIONS.getKnownLocales(); + languageBox.setModel(new DefaultComboBoxModel(known.toArray())); + + final Locale current = TRANSLATIONS.getLocale(); + languageBox.setSelectedItem(known.stream().filter(locate -> locate.locale.equals(current)).findFirst().orElse(known.get(0))); + languageBox.addActionListener(e -> TRANSLATIONS.setLocale(((L10nManager.LocaleSelection)languageBox.getSelectedItem()).locale, true)); + + JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION, null, new Object[] { + TRANSLATIONS.button("installer.button.proceed"), TRANSLATIONS.button("installer.button.cancel"), languageBox + }); // Attempt to change the OK button to a Proceed button // Use index 1 (the buttons panel) as 0 is this panel - proceedButton = Arrays.stream(((JPanel) optionPane.getComponents()[1]).getComponents()) + final JPanel buttonPanel = (JPanel) optionPane.getComponents()[1]; + final List buttons = Arrays.stream(buttonPanel.getComponents()) .filter(comp -> comp instanceof JButton) .map(JButton.class::cast) - .filter(btn -> btn.getText().equals("OK")) - .findFirst(); - proceedButton.ifPresent(button -> button.setText("Proceed")); + .collect(Collectors.toList()); + if (buttons.size() == 2) { + proceedButton = Optional.of(buttons.get(0)); + buttons.get(0).addActionListener(e -> optionPane.setValue(JOptionPane.OK_OPTION)); + buttons.get(1).addActionListener(e -> optionPane.setValue(JOptionPane.OK_CANCEL_OPTION)); + } - dialog = optionPane.createDialog("NeoForge installer"); + dialog = optionPane.createDialog(""); + TRANSLATIONS.translate(dialog, new TranslationTarget<>(Dialog::setTitle), "installer.window.title", profile.getProfile()); dialog.setIconImages(Images.getWindowIcons()); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setVisible(true); int result = (Integer) (optionPane.getValue() != null ? optionPane.getValue() : -1); if (result == JOptionPane.OK_OPTION) { - ProgressFrame prog = new ProgressFrame(monitor, "Installing " + profile.getVersion(), Thread.currentThread()::interrupt); + ProgressFrame prog = new ProgressFrame(monitor, Thread.currentThread()::interrupt, "installer.frame.installing", profile.getProfile(), profile.getVersion()); SimpleInstaller.hookStdOut(prog); Predicate optPred = input -> { Optional ent = this.optionals.stream().filter(e -> e.lib.getArtifact().equals(input)).findFirst(); @@ -413,10 +342,10 @@ public void run(ProgressCallback monitor) if (action.run(targetDir, optPred, installer)) { prog.start("Finished!"); prog.getGlobalProgress().percentageProgress(1); - JOptionPane.showMessageDialog(null, action.getSuccessMessage(), "Complete", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(null, TRANSLATIONS.translate(action.getSuccessMessage()), TRANSLATIONS.translate("installer.installation.complete"), JOptionPane.INFORMATION_MESSAGE); } } catch (ActionCanceledException e) { - JOptionPane.showMessageDialog(null, "Installation Canceled", "Forge Installer", JOptionPane.WARNING_MESSAGE); + JOptionPane.showMessageDialog(null, TRANSLATIONS.translate("installer.installation.cancelled"), dialog.getTitle(), JOptionPane.WARNING_MESSAGE); } catch (Exception e) { JOptionPane.showMessageDialog(null, "There was an exception running task: " + e.toString(), "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); @@ -424,6 +353,8 @@ public void run(ProgressCallback monitor) prog.dispose(); SimpleInstaller.hookStdOut(monitor); } + } else if (result == JOptionPane.OK_CANCEL_OPTION) { + System.exit(0); } dialog.dispose(); } diff --git a/src/main/java/net/minecraftforge/installer/ui/L10nManager.java b/src/main/java/net/minecraftforge/installer/ui/L10nManager.java new file mode 100644 index 0000000..374ae94 --- /dev/null +++ b/src/main/java/net/minecraftforge/installer/ui/L10nManager.java @@ -0,0 +1,314 @@ +/* + * Installer + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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 + */ +package net.minecraftforge.installer.ui; + +import org.jetbrains.annotations.NotNull; + +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JRadioButton; +import java.awt.Component; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.Set; + +public class L10nManager { + private static final ResourceBundle.Control CONTROL = new ResourceBundle.Control() { + @Override + public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { + if (format.equals("java.class")) { + return super.newBundle(baseName, locale, format, loader, reload); + } else if (!format.equals("java.properties")) { + throw new IllegalArgumentException("unknown format: " + format); + } + + // Copied from the javadocs of ResourceBundle.Control + final String bundleName = toBundleName(baseName, locale); + + if (bundleName.contains("://")) { + return null; + } + final String resourceName = toResourceName(bundleName, "xml"); + + InputStream is = null; + if (reload) { + URL url = loader.getResource(resourceName); + if (url != null) { + URLConnection connection = url.openConnection(); + if (connection != null) { + // Disable caches to get fresh data for + // reloading. + connection.setUseCaches(false); + is = connection.getInputStream(); + } + } + } else { + is = loader.getResourceAsStream(resourceName); + } + + if (is == null) { + return null; + } + + try (final InputStream i = is) { + final Properties props = new Properties(); + props.loadFromXML(i); + final Map lookup = new HashMap(props); + return new ResourceBundle() { + + @Override + protected Object handleGetObject(@NotNull String key) { + return lookup.get(key); + } + + @NotNull + @Override + public Enumeration getKeys() { + return new MergedEnumeration(lookup.keySet(), (parent != null) ? parent.getKeys() : null); + } + + @NotNull + @Override + protected Set handleKeySet() { + return lookup.keySet(); + } + }; + } + } + }; + + private final Map, PropertyChangeListener>> components = new IdentityHashMap<>(); + private Locale locale; + private ResourceBundle bundle; + private final String bundleName; + private final Path settingsFile; + private final List known; + + public L10nManager(String bundleName, Path settingsFile) { + this.bundleName = bundleName; + this.settingsFile = settingsFile; + this.known = Collections.unmodifiableList(computeKnownLocales()); + + if (Files.exists(this.settingsFile)) { + try (final InputStream is = Files.newInputStream(this.settingsFile)) { + final Properties props = new Properties(); + props.load(is); + final String lang = props.getProperty("language"); + if (lang == null) { + setDefaultLocale(); + } else { + setLocale(Locale.forLanguageTag(lang), false); + } + } catch (Exception exception) { + System.err.println("Failed to read settings file: " + exception); + setDefaultLocale(); + } + } else { + setDefaultLocale(); + } + } + + private void setDefaultLocale() { + final String expected = Locale.getDefault().toLanguageTag(); + setLocale(known.stream().filter(se -> se.locale.toLanguageTag().equals(expected)) + .findFirst().map(l -> l.locale).orElse(Locale.ENGLISH), false); + } + + public JButton button(String key, Object... args) { + return translate(new JButton(), TranslationTarget.BUTTON_TEXT, key, args); + } + + public JLabel label(String key, Object... args) { + return translate(new JLabel(), TranslationTarget.LABEL_TEXT, key, args); + } + + public JRadioButton radioButton(AbstractAction action, String key, Object... args) { + final JRadioButton button = new JRadioButton(); + button.setAction(action); + return translate(button, TranslationTarget.BUTTON_TEXT, key, args); + } + + public T setTooltip(T component, String key, Object... args) { + return translate(component, TranslationTarget.TOOLTIP, key, args); + } + + @SuppressWarnings("unchecked") + public T translate(T component, TranslationTarget target, String key, Object... args) { + final Map, PropertyChangeListener> listeners = (Map) components.computeIfAbsent(component, k -> new HashMap()); + if (listeners.containsKey(target)) { + component.removePropertyChangeListener(listeners.get(target)); + } + final PropertyChangeListener pcl = evt -> target.setter.accept(component, translate(key, args)); + component.addPropertyChangeListener("locale", pcl); + if (component.getLocale().equals(locale)) { + target.setter.accept(component, translate(key, args)); + } else { + component.setLocale(locale); + } + listeners.put(target, pcl); + return component; + } + + public synchronized String translate(String key, Object... args) { + try { + return String.format(bundle.getString(key), args); + } catch (MissingResourceException ignored) { + return String.format(key, args); + } + } + + public String translate(TranslatedMessage message) { + return translate(message.key, message.arguments); + } + + public synchronized void setLocale(Locale locale, boolean write) { + this.locale = locale; + this.bundle = ResourceBundle.getBundle(bundleName, locale, CONTROL); + + components.keySet().forEach(comp -> comp.setLocale(locale)); + + if (write) { + try { + Files.createDirectories(settingsFile.getParent()); + try (final OutputStream os = Files.newOutputStream(settingsFile)) { + final Properties props = new Properties(); + props.put("language", locale.toLanguageTag()); + props.store(os, "NeoForge installer settings file"); + } + Files.setAttribute(settingsFile, "dos:hidden", true); + } catch (Exception exception) { + System.err.println("Failed to write settings file: " + exception); + } + } + } + + public synchronized Locale getLocale() { + return locale; + } + + public List getKnownLocales() { + return known; + } + + private List computeKnownLocales() { + try { + final Set names = new HashSet<>(); + final List selections = new ArrayList<>(); + for (final Locale locale : Locale.getAvailableLocales()) { + final InputStream is = L10nManager.class.getResourceAsStream("/" + CONTROL.toBundleName(bundleName, locale) + ".xml"); + if (is != null) { + try { + final Properties properties = new Properties(); + properties.loadFromXML(is); + String name = locale.equals(Locale.ROOT) ? "english" : locale.getDisplayName(locale); + name = Character.toUpperCase(name.charAt(0)) + name.substring(1); + // Only root can be named English + if (names.add(name) && (!name.equals("English") || locale.equals(Locale.ROOT))) { + selections.add(new LocaleSelection(locale, name)); + } + } finally { + is.close(); + } + } + } + return selections; + } catch (Exception exception) { + throw new RuntimeException("Failed to read languages: " + exception.getMessage(), exception); + } + } + + public static final class LocaleSelection { + public final Locale locale; + public final String name; + + private LocaleSelection(Locale locale, String name) { + this.locale = locale; + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + private static final class MergedEnumeration implements Enumeration { + + private final Set set; + private final Iterator iterator; + private final Enumeration enumeration; + + public MergedEnumeration(Set set, Enumeration enumeration) { + this.set = set; + this.iterator = set.iterator(); + this.enumeration = enumeration; + } + + String next = null; + + public boolean hasMoreElements() { + if (next == null) { + if (iterator.hasNext()) { + next = iterator.next(); + } else if (enumeration != null) { + while (next == null && enumeration.hasMoreElements()) { + next = enumeration.nextElement(); + if (set.contains(next)) { + next = null; + } + } + } + } + return next != null; + } + + public String nextElement() { + if (hasMoreElements()) { + String result = next; + next = null; + return result; + } else { + throw new NoSuchElementException(); + } + } + } +} diff --git a/src/main/java/net/minecraftforge/installer/ProgressFrame.java b/src/main/java/net/minecraftforge/installer/ui/ProgressFrame.java similarity index 93% rename from src/main/java/net/minecraftforge/installer/ProgressFrame.java rename to src/main/java/net/minecraftforge/installer/ui/ProgressFrame.java index 082e39a..a0b40f8 100644 --- a/src/main/java/net/minecraftforge/installer/ProgressFrame.java +++ b/src/main/java/net/minecraftforge/installer/ui/ProgressFrame.java @@ -16,21 +16,14 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package net.minecraftforge.installer; +package net.minecraftforge.installer.ui; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; -import javax.swing.JButton; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.WindowConstants; +import javax.swing.*; import net.minecraftforge.installer.actions.ProgressCallback; @@ -49,14 +42,14 @@ public class ProgressFrame extends JFrame implements ProgressCallback private final ProgressBar stepProgressController; private final JTextArea consoleArea; - public ProgressFrame(ProgressCallback parent, String title, Runnable canceler) + public ProgressFrame(ProgressCallback parent, Runnable canceler, String titleKey, Object... titleArgs) { int gridY = 0; this.parent = parent; setResizable(false); - setTitle(title); + InstallerPanel.TRANSLATIONS.translate(this, new TranslationTarget<>(JFrame::setTitle), titleKey, titleArgs); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setBounds(100, 100, 600, 400); setContentPane(panel); @@ -95,9 +88,8 @@ public ProgressFrame(ProgressCallback parent, String title, Runnable canceler) gbc_stepProgress.gridy = gridY++; panel.add(stepProgress, gbc_stepProgress); - JButton btnCancel = new JButton("Cancel"); - btnCancel.addActionListener(e -> - { + JButton btnCancel = InstallerPanel.TRANSLATIONS.button("installer.button.cancel"); + btnCancel.addActionListener(e -> { canceler.run(); ProgressFrame.this.dispose(); }); diff --git a/src/main/java/net/minecraftforge/installer/ui/TranslatedMessage.java b/src/main/java/net/minecraftforge/installer/ui/TranslatedMessage.java new file mode 100644 index 0000000..e947246 --- /dev/null +++ b/src/main/java/net/minecraftforge/installer/ui/TranslatedMessage.java @@ -0,0 +1,29 @@ +/* + * Installer + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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 + */ +package net.minecraftforge.installer.ui; + +public class TranslatedMessage { + final String key; + final Object[] arguments; + + public TranslatedMessage(String key, Object... arguments) { + this.key = key; + this.arguments = arguments; + } +} diff --git a/src/main/java/net/minecraftforge/installer/ui/TranslationTarget.java b/src/main/java/net/minecraftforge/installer/ui/TranslationTarget.java new file mode 100644 index 0000000..112bbfd --- /dev/null +++ b/src/main/java/net/minecraftforge/installer/ui/TranslationTarget.java @@ -0,0 +1,44 @@ +/* + * Installer + * Copyright (c) 2016-2018. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser 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 + */ +package net.minecraftforge.installer.ui; + +import javax.swing.*; +import java.awt.*; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +@SuppressWarnings({"unchecked", "rawtypes"}) +public final class TranslationTarget { + public static final TranslationTarget LABEL_TEXT = new TranslationTarget<>(JLabel::setText); + public static final TranslationTarget BUTTON_TEXT = new TranslationTarget<>(AbstractButton::setText); + public static final TranslationTarget TOOLTIP = new TranslationTarget<>(JComponent::setToolTipText); + + private static final Map HTML_TARGETS = new IdentityHashMap<>(); + + public static TranslationTarget html(TranslationTarget target) { + return (TranslationTarget) HTML_TARGETS.computeIfAbsent(target, k -> new TranslationTarget((c, s) -> k.setter.accept(c, "" + s + ""))); + } + + final BiConsumer setter; + + public TranslationTarget(BiConsumer setter) { + this.setter = setter; + } +} diff --git a/src/main/resources/neoforged/installer.xml b/src/main/resources/neoforged/installer.xml new file mode 100644 index 0000000..55f1b11 --- /dev/null +++ b/src/main/resources/neoforged/installer.xml @@ -0,0 +1,36 @@ + + + + Version: + Path to the Minecraft installation directory + Select an alternative Minecraft directory + + Proceed + Cancel + + Install client + Install a new profile to the Mojang client launcher + Successfully installed client profile %s for version %s into launcher + Successfully installed client profile %s for version %s into launcher, and downloaded %d libraries + + Install server + Create a new modded server installation + Successfully downloaded minecraft server and installed %s + Successfully downloaded minecraft server, downloaded %d libraries and installed %s + + Extract + Extract the contained jar file + Extracted successfully + + %s Installer + Installing %s version %s + + Installation Complete + Installation Cancelled + + The specified path needs to be a directory + There are already files in the target directory + The specified directory does not exist + The specified directory does not exist<br>It will be created + The directory is missing a launcher profile. Please run the Minecraft launcher first +