From 72d52dc317b562f728b772f058ab2c5059a2a618 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 4 Feb 2018 22:38:24 -0500 Subject: [PATCH] framework for translations (#55) sets up all the plumbing to send strings to CrowdIn for translation and incorporate the results --- .gitignore | 1 + Makefile | 22 ++- crowdin.yml | 3 + embroider.py | 84 +++++------ embroider_params.py | 52 ++++--- embroider_simulate.py | 6 +- git-hooks/README.md | 1 + git-hooks/pre-commit | 4 + inkstitch.py | 32 +++- messages.po | 329 ++++++++++++++++++++++++++++++++++++++++++ translations/.keep | 0 11 files changed, 461 insertions(+), 73 deletions(-) create mode 100644 crowdin.yml create mode 100644 git-hooks/README.md create mode 100755 git-hooks/pre-commit create mode 100644 messages.po create mode 100644 translations/.keep diff --git a/.gitignore b/.gitignore index b599c0a16..5b6925eb4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.tar.gz dist/ build/ +locales/ # For development, I symlink into my clone, so I have to have a copy of libembroidery. libembroidery.py diff --git a/Makefile b/Makefile index e45fabb87..f8c4d35f0 100644 --- a/Makefile +++ b/Makefile @@ -5,10 +5,30 @@ VERSION:=$(TRAVIS_BRANCH) OS:=$(shell uname) ARCH:=$(shell uname -m) -dist: distclean +dist: distclean locales bin/build-dist $(EXTENSIONS) cp *.inx dist + mv locales dist/bin cd dist; tar zcf ../inkstitch-$(VERSION)-$(OS)-$(ARCH).tar.gz * distclean: rm -rf build dist *.spec *.tar.gz + +messages.po: embroider*.py inkstitch.py + rm -f messages.po + xgettext embroider*.py inkstitch.py + +.PHONY: locales +locales: + # message files will look like this: + # translations/messages-en_US.po + if ls translations/*.po > /dev/null 2>&1; then \ + for po in translations/*.po; do \ + lang=$${po%.po}; \ + lang=$${lang#messages-}; \ + mkdir -p locales/$$lang/LC_MESSAGES/; \ + msgfmt $$po -o locales/$$lang/LC_MESSAGES/inkstitch.mo; \ + done; \ + else \ + mkdir locales; \ + fi diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..be40a011e --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: messages.po + translation: /translations/messages_%locale_with_underscore%.po diff --git a/embroider.py b/embroider.py index da0350b20..1c65ef4d3 100644 --- a/embroider.py +++ b/embroider.py @@ -35,20 +35,19 @@ import networkx from pprint import pformat import inkstitch -from inkstitch import cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform - +from inkstitch import _, cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform class Fill(EmbroideryElement): def __init__(self, *args, **kwargs): super(Fill, self).__init__(*args, **kwargs) @property - @param('auto_fill', 'Manually routed fill stitching', type='toggle', inverse=True, default=True) + @param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True) def auto_fill(self): return self.get_boolean_param('auto_fill', True) @property - @param('angle', 'Angle of lines of stitches', unit='deg', type='float', default=0) + @param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0) @cache def angle(self): return math.radians(self.get_float_param('angle', 0)) @@ -58,12 +57,12 @@ class Fill(EmbroideryElement): return self.get_style("fill") @property - @param('flip', 'Flip fill (start right-to-left)', type='boolean', default=False) + @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) def flip(self): return self.get_boolean_param("flip", False) @property - @param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float', default=0.25) + @param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25) def row_spacing(self): return max(self.get_float_param("row_spacing_mm", 0.25), 0.01) @@ -72,12 +71,12 @@ class Fill(EmbroideryElement): return self.get_float_param("end_row_spacing_mm") @property - @param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float', default=3.0) + @param('max_stitch_length_mm', _('Maximum fill stitch length'), unit='mm', type='float', default=3.0) def max_stitch_length(self): return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.01) @property - @param('staggers', 'Stagger rows this many times before repeating', type='int', default=4) + @param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4) def staggers(self): return self.get_int_param("staggers", 4) @@ -371,7 +370,7 @@ class MaxQueueLengthExceeded(Exception): class AutoFill(Fill): @property - @param('auto_fill', 'Automatically routed fill stitching', type='toggle', default=True) + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) def auto_fill(self): return self.get_boolean_param('auto_fill', True) @@ -390,17 +389,17 @@ class AutoFill(Fill): return False @property - @param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float', default=1.5) + @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @property - @param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay', default=False) + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False) def fill_underlay(self): return self.get_boolean_param("fill_underlay", default=False) @property - @param('fill_underlay_angle', 'Fill angle (default: fill angle + 90 deg)', unit='deg', group='AutoFill Underlay', type='float') + @param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float') @cache def fill_underlay_angle(self): underlay_angle = self.get_float_param("fill_underlay_angle") @@ -411,13 +410,13 @@ class AutoFill(Fill): return self.angle + math.pi / 2.0 @property - @param('fill_underlay_row_spacing_mm', 'Row spacing (default: 3x fill row spacing)', unit='mm', group='AutoFill Underlay', type='float') + @param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float') @cache def fill_underlay_row_spacing(self): return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 @property - @param('fill_underlay_max_stitch_length_mm', 'Max stitch length', unit='mm', group='AutoFill Underlay', type='float') + @param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float') @cache def fill_underlay_max_stitch_length(self): return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length @@ -538,7 +537,7 @@ class AutoFill(Fill): if not networkx.is_eulerian(graph): - raise Exception("something went wrong: graph is not eulerian") + raise Exception(_("Unable to autofill. This most often happens because your shape is made up of multiple sections that aren't connected.")) return graph @@ -713,7 +712,7 @@ class AutoFill(Fill): result = self.find_loop(graph, nodes_visited) if not result: - print >> sys.stderr, "Unexpected error filling region. Please send your SVG to lexelby@github." + print >> sys.stderr, _("Unexpected error while generating fill stitches. Please send your SVG file to lexelby@github.") break loop, segments = result @@ -882,7 +881,7 @@ class AutoFill(Fill): class Stroke(EmbroideryElement): @property - @param('satin_column', 'Satin along paths', type='toggle', inverse=True) + @param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True) def satin_column(self): return self.get_boolean_param("satin_column") @@ -905,18 +904,18 @@ class Stroke(EmbroideryElement): return self.get_style("stroke-dasharray") is not None @property - @param('running_stitch_length_mm', 'Running stitch length', unit='mm', type='float', default=1.5) + @param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5) def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) @property - @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float', default=0.4) + @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) @cache def zigzag_spacing(self): return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) @property - @param('repeats', 'Repeats', type='int', default="1") + @param('repeats', _('Repeats'), type='int', default="1") def repeats(self): return self.get_int_param("repeats", 1) @@ -997,7 +996,7 @@ class SatinColumn(EmbroideryElement): super(SatinColumn, self).__init__(*args, **kwargs) @property - @param('satin_column', 'Custom satin column', type='toggle') + @param('satin_column', _('Custom satin column'), type='toggle') def satin_column(self): return self.get_boolean_param("satin_column") @@ -1006,13 +1005,13 @@ class SatinColumn(EmbroideryElement): return self.get_style("stroke") @property - @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float', default=0.4) + @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) def zigzag_spacing(self): # peak-to-peak distance between zigzags return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) @property - @param('pull_compensation_mm', 'Pull compensation', unit='mm', type='float') + @param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float') def pull_compensation(self): # In satin stitch, the stitches have a tendency to pull together and # narrow the entire column. We can compensate for this by stitching @@ -1020,47 +1019,47 @@ class SatinColumn(EmbroideryElement): return self.get_float_param("pull_compensation_mm", 0) @property - @param('contour_underlay', 'Contour underlay', type='toggle', group='Contour Underlay') + @param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) def contour_underlay(self): # "Contour underlay" is stitching just inside the rectangular shape # of the satin column; that is, up one side and down the other. return self.get_boolean_param("contour_underlay") @property - @param('contour_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Contour Underlay', type='float', default=1.5) + @param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5) def contour_underlay_stitch_length(self): return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01) @property - @param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float', default=0.4) + @param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4) def contour_underlay_inset(self): # how far inside the edge of the column to stitch the underlay return self.get_float_param("contour_underlay_inset_mm", 0.4) @property - @param('center_walk_underlay', 'Center-walk underlay', type='toggle', group='Center-Walk Underlay') + @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) def center_walk_underlay(self): # "Center walk underlay" is stitching down and back in the centerline # between the two sides of the satin column. return self.get_boolean_param("center_walk_underlay") @property - @param('center_walk_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Center-Walk Underlay', type='float', default=1.5) + @param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5) def center_walk_underlay_stitch_length(self): return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01) @property - @param('zigzag_underlay', 'Zig-zag underlay', type='toggle', group='Zig-zag Underlay') + @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) def zigzag_underlay(self): return self.get_boolean_param("zigzag_underlay") @property - @param('zigzag_underlay_spacing_mm', 'Zig-Zag spacing (peak-to-peak)', unit='mm', group='Zig-zag Underlay', type='float', default=3) + @param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3) def zigzag_underlay_spacing(self): return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01) @property - @param('zigzag_underlay_inset_mm', 'Inset amount (default: half of contour underlay inset)', unit='mm', group='Zig-zag Underlay', type='float') + @param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float') def zigzag_underlay_inset(self): # how far in from the edge of the satin the points in the zigzags # should be @@ -1106,14 +1105,16 @@ class SatinColumn(EmbroideryElement): for rail in rails: if not rail.is_simple: - self.fatal("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.") + self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.")) # handle null intersections here? linestrings = shapely.ops.split(rail, rungs) + print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs] if len(linestrings.geoms) < len(rungs.geoms) + 1: - print >> dbg, [str(rail) for rail in rails], [str(rung) for rung in rungs] - self.fatal("Expected %d linestrings, got %d" % (len(rungs.geoms) + 1, len(linestrings.geoms))) + self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once.")) + elif len(linestrings.geoms) > len(rungs.geoms) + 1: + self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once.")) paths = [[inkstitch.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms] result.append(paths) @@ -1154,11 +1155,12 @@ class SatinColumn(EmbroideryElement): node_id = self.node.get("id") if self.get_style("fill") is not None: - self.fatal("satin column: object %s has a fill (but should not)" % node_id) + self.fatal(_("satin column: object %s has a fill (but should not)") % node_id) if len(self.csp) == 2: if len(self.csp[0]) != len(self.csp[1]): - self.fatal("satin column: object %s has two paths with an unequal number of points (%s and %s)" % (node_id, len(self.csp[0]), len(self.csp[1]))) + self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") % \ + dict(id=node_id, length1=len(self.csp[0]), length2=len(self.csp[1]))) def offset_points(self, pos1, pos2, offset_px): # Expand or contract two points about their midpoint. This is @@ -1690,7 +1692,7 @@ class Embroider(inkex.Effect): action="store", type="int", dest="max_backups", default=5, help="Max number of backups of output files to keep.") - self.OptionParser.usage += "\n\nSeeing a 'no such option' message? Please restart Inkscape to fix." + self.OptionParser.usage += _("\n\nSeeing a 'no such option' message? Please restart Inkscape to fix.") def get_output_path(self): if self.options.output_file: @@ -1738,10 +1740,10 @@ class Embroider(inkex.Effect): if not self.elements: if self.selected: - inkex.errormsg("No embroiderable paths selected.") + inkex.errormsg(_("No embroiderable paths selected.")) else: - inkex.errormsg("No embroiderable paths found in document.") - inkex.errormsg("Tip: use Path -> Object to Path to convert non-paths before embroidering.") + inkex.errormsg(_("No embroiderable paths found in document.")) + inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering.")) return if self.options.hide_layers: @@ -1753,7 +1755,7 @@ class Embroider(inkex.Effect): new_layer = inkex.etree.SubElement(self.document.getroot(), SVG_GROUP_TAG, {}) new_layer.set('id', self.uniqueId("embroidery")) - new_layer.set(inkex.addNS('label', 'inkscape'), 'Embroidery') + new_layer.set(inkex.addNS('label', 'inkscape'), _('Embroidery')) new_layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') emit_inkscape(new_layer, stitches) diff --git a/embroider_params.py b/embroider_params.py index 3725a39df..1bb6620e5 100644 --- a/embroider_params.py +++ b/embroider_params.py @@ -13,12 +13,14 @@ import wx from wx.lib.scrolledpanel import ScrolledPanel from collections import defaultdict import inkex -from inkstitch import Param, EmbroideryElement, get_nodes +import inkstitch +from inkstitch import _, Param, EmbroideryElement, get_nodes from embroider import Fill, AutoFill, Stroke, SatinColumn from functools import partial from itertools import groupby from embroider_simulate import EmbroiderySimulator + def presets_path(): try: import appdirs @@ -237,17 +239,22 @@ class ParamsTab(ScrolledPanel): preset[name] = input.GetValue() def update_description(self): - description = "These settings will be applied to %d object%s." % \ - (len(self.nodes), "s" if len(self.nodes) != 1 else "") + if len(self.nodes) == 1: + description = _("These settings will be applied to 1 object.") + else: + description = _("These settings will be applied to %d objects.") % len(self.nodes) if any(len(param.values) > 1 for param in self.params): - description += "\n • Some settings had different values across objects. Select a value from the dropdown or enter a new one." + description += "\n • " + _("Some settings had different values across objects. Select a value from the dropdown or enter a new one.") if self.dependent_tabs: - description += "\n • Disabling this tab will disable the following %d tabs." % len(self.dependent_tabs) + if len(self.dependent_tabs) == 1: + description += "\n • " + _("Disabling this tab will disable the following %d tabs.") % len(self.dependent_tabs) + else: + description += "\n • " + _("Disabling this tab will disable the following tab.") if self.paired_tab: - description += "\n • Enabling this tab will disable %s and vice-versa." % self.paired_tab.name + description += "\n • " + _("Enabling this tab will disable %s and vice-versa.") % self.paired_tab.name self.description_text = description @@ -277,7 +284,7 @@ class ParamsTab(ScrolledPanel): # just to add space around the settings box = wx.BoxSizer(wx.VERTICAL) - summary_box = wx.StaticBox(self, wx.ID_ANY, label="Inkscape objects") + summary_box = wx.StaticBox(self, wx.ID_ANY, label=_("Inkscape objects")) sizer = wx.StaticBoxSizer(summary_box, wx.HORIZONTAL) # sizer = wx.BoxSizer(wx.HORIZONTAL) self.description = wx.StaticText(self, style=wx.TE_WORDWRAP) @@ -335,7 +342,7 @@ class SettingsFrame(wx.Frame): self.tabs_factory = kwargs.pop('tabs_factory', []) self.cancel_hook = kwargs.pop('on_cancel', None) wx.Frame.__init__(self, None, wx.ID_ANY, - "Embroidery Params" + _("Embroidery Params") ) self.notebook = wx.Notebook(self, wx.ID_ANY) self.tabs = self.tabs_factory(self.notebook) @@ -349,31 +356,31 @@ class SettingsFrame(wx.Frame): wx.CallLater(1000, self.update_simulator) - self.presets_box = wx.StaticBox(self, wx.ID_ANY, label="Presets") + self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets")) self.preset_chooser = wx.ComboBox(self, wx.ID_ANY) self.update_preset_list() - self.load_preset_button = wx.Button(self, wx.ID_ANY, "Load") + self.load_preset_button = wx.Button(self, wx.ID_ANY, _("Load")) self.load_preset_button.Bind(wx.EVT_BUTTON, self.load_preset) - self.add_preset_button = wx.Button(self, wx.ID_ANY, "Add") + self.add_preset_button = wx.Button(self, wx.ID_ANY, _("Add")) self.add_preset_button.Bind(wx.EVT_BUTTON, self.add_preset) - self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, "Overwrite") + self.overwrite_preset_button = wx.Button(self, wx.ID_ANY, _("Overwrite")) self.overwrite_preset_button.Bind(wx.EVT_BUTTON, self.overwrite_preset) - self.delete_preset_button = wx.Button(self, wx.ID_ANY, "Delete") + self.delete_preset_button = wx.Button(self, wx.ID_ANY, _("Delete")) self.delete_preset_button.Bind(wx.EVT_BUTTON, self.delete_preset) - self.cancel_button = wx.Button(self, wx.ID_ANY, "Cancel") + self.cancel_button = wx.Button(self, wx.ID_ANY, _("Cancel")) self.cancel_button.Bind(wx.EVT_BUTTON, self.cancel) self.Bind(wx.EVT_CLOSE, self.cancel) - self.use_last_button = wx.Button(self, wx.ID_ANY, "Use Last Settings") + self.use_last_button = wx.Button(self, wx.ID_ANY, _("Use Last Settings")) self.use_last_button.Bind(wx.EVT_BUTTON, self.use_last) - self.apply_button = wx.Button(self, wx.ID_ANY, "Apply and Quit") + self.apply_button = wx.Button(self, wx.ID_ANY, _("Apply and Quit")) self.apply_button.Bind(wx.EVT_BUTTON, self.apply) self.__set_properties() @@ -418,7 +425,7 @@ class SettingsFrame(wx.Frame): max_height = screen_rect.GetHeight() try: - self.simulate_window = EmbroiderySimulator(None, -1, "Embroidery Simulator", + self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"), simulator_pos, size=(300, 300), patches=patches, @@ -432,11 +439,11 @@ class SettingsFrame(wx.Frame): try: # a window may have been created, so we need to destroy it # or the app will never exit - wx.Window.FindWindowByName("Embroidery Simulator").Destroy() + wx.Window.FindWindowByName("Preview").Destroy() except: pass - info_dialog(self, error, "Internal Error") + info_dialog(self, error, _("Internal Error")) self.simulate_window.Show() wx.CallLater(10, self.Raise) @@ -489,13 +496,13 @@ class SettingsFrame(wx.Frame): if preset_name: return preset_name else: - info_dialog(self, "Please enter or select a preset name first.", caption='Preset') + info_dialog(self, _("Please enter or select a preset name first."), caption=_('Preset')) return def check_and_load_preset(self, preset_name): preset = load_preset(preset_name) if not preset: - info_dialog(self, 'Preset "%s" not found.' % preset_name, caption='Preset') + info_dialog(self, _('Preset "%s" not found.') % preset_name, caption=_('Preset')) return preset @@ -523,7 +530,7 @@ class SettingsFrame(wx.Frame): return if not overwrite and load_preset(preset_name): - info_dialog(self, 'Preset "%s" already exists. Please use another name or press "Overwrite"' % preset_name, caption='Preset') + info_dialog(self, _('Preset "%s" already exists. Please use another name or press "Overwrite"') % preset_name, caption=_('Preset')) save_preset(preset_name, self.get_preset_data()) self.update_preset_list() @@ -596,7 +603,6 @@ class SettingsFrame(wx.Frame): def __set_properties(self): # begin wxGlade: MyFrame.__set_properties - self.SetTitle("Embroidery Parameters") self.notebook.SetMinSize((800, 600)) self.preset_chooser.SetSelection(-1) # end wxGlade diff --git a/embroider_simulate.py b/embroider_simulate.py index 063a2f35d..979668b05 100644 --- a/embroider_simulate.py +++ b/embroider_simulate.py @@ -6,8 +6,10 @@ import inkex import simplestyle import colorsys +import inkstitch from inkstitch import PIXELS_PER_MM -from embroider import patches_to_stitches, get_elements, elements_to_patches +from embroider import _, patches_to_stitches, get_elements, elements_to_patches + class EmbroiderySimulator(wx.Frame): def __init__(self, *args, **kwargs): @@ -298,7 +300,7 @@ class SimulateEffect(inkex.Effect): def effect(self): patches = elements_to_patches(get_elements(self)) app = wx.App() - frame = EmbroiderySimulator(None, -1, "Embroidery Simulation", wx.DefaultPosition, size=(1000, 1000), patches=patches) + frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), patches=patches) app.SetTopWindow(frame) frame.Show() wx.CallAfter(frame.go) diff --git a/git-hooks/README.md b/git-hooks/README.md new file mode 100644 index 000000000..910f476ad --- /dev/null +++ b/git-hooks/README.md @@ -0,0 +1 @@ +Files in this directory are meant to be symlinked or copied into your local clone's .git/hooks directory. diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit new file mode 100755 index 000000000..f01b516ea --- /dev/null +++ b/git-hooks/pre-commit @@ -0,0 +1,4 @@ +#!/bin/bash + +make messages.po | grep -v 'is up to date\.$' +git add messages.po diff --git a/inkstitch.py b/inkstitch.py index 06e26bb34..c53781fc2 100644 --- a/inkstitch.py +++ b/inkstitch.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # http://www.achatina.de/sewing/main/TECHNICL.HTM +import os import sys +import gettext from copy import deepcopy import math import libembroidery @@ -31,10 +33,28 @@ EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) dbg = open("/tmp/embroider-debug.txt", "w") +_ = lambda message: message + # simplify use of lru_cache decorator def cache(*args, **kwargs): return lru_cache(maxsize=None)(*args, **kwargs) +def localize(): + if getattr(sys, 'frozen', False): + # we are in a pyinstaller installation + locale_dir = sys._MEIPASS + else: + locale_dir = os.path.dirname(__file__) + + locale_dir = os.path.join(locale_dir, 'locales') + + translation = gettext.translation("inkstitch", locale_dir, fallback=True) + + global _ + _ = translation.gettext + +localize() + # cribbed from inkscape-silhouette def parse_length_with_units( str ): @@ -71,7 +91,7 @@ def parse_length_with_units( str ): try: v = float( s ) except: - raise ValueError("parseLengthWithUnits: unknown unit %s" % s) + raise ValueError(_("parseLengthWithUnits: unknown unit %s") % s) return v, u @@ -95,7 +115,7 @@ def convert_length(length): # open an old document, inkscape will add a viewbox for you. return value * 96 - raise ValueError("Unknown unit: %s" % units) + raise ValueError(_("Unknown unit: %s") % units) @cache @@ -298,8 +318,8 @@ class EmbroideryElement(object): @property @param('trim_after', - 'TRIM after', - tooltip='Trim thread after this object (for supported machines and file formats)', + _('TRIM after'), + tooltip=_('Trim thread after this object (for supported machines and file formats)'), type='boolean', default=False, sort_index=1000) @@ -308,8 +328,8 @@ class EmbroideryElement(object): @property @param('stop_after', - 'STOP after', - tooltip='Add STOP instruction after this object (for supported machines and file formats)', + _('STOP after'), + tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'), type='boolean', default=False, sort_index=1000) diff --git a/messages.po b/messages.po new file mode 100644 index 000000000..2f53b9ce8 --- /dev/null +++ b/messages.po @@ -0,0 +1,329 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-02-04 16:24-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: embroider.py:45 +msgid "Manually routed fill stitching" +msgstr "" + +#: embroider.py:50 +msgid "Angle of lines of stitches" +msgstr "" + +#: embroider.py:60 +msgid "Flip fill (start right-to-left)" +msgstr "" + +#: embroider.py:65 +msgid "Spacing between rows" +msgstr "" + +#: embroider.py:74 +msgid "Maximum fill stitch length" +msgstr "" + +#: embroider.py:79 +msgid "Stagger rows this many times before repeating" +msgstr "" + +#: embroider.py:373 +msgid "Automatically routed fill stitching" +msgstr "" + +#: embroider.py:392 +msgid "Running stitch length (traversal between sections)" +msgstr "" + +#: embroider.py:397 +msgid "Underlay" +msgstr "" + +#: embroider.py:397 embroider.py:402 embroider.py:413 embroider.py:419 +msgid "AutoFill Underlay" +msgstr "" + +#: embroider.py:402 +msgid "Fill angle (default: fill angle + 90 deg)" +msgstr "" + +#: embroider.py:413 +msgid "Row spacing (default: 3x fill row spacing)" +msgstr "" + +#: embroider.py:419 +msgid "Max stitch length" +msgstr "" + +#: embroider.py:540 +msgid "" +"Unable to autofill. This most often happens because your shape is made up " +"of multiple sections that aren't connected." +msgstr "" + +#: embroider.py:715 +msgid "" +"Unexpected error while generating fill stitches. Please send your SVG file " +"to lexelby@github." +msgstr "" + +#: embroider.py:884 +msgid "Satin stitch along paths" +msgstr "" + +#: embroider.py:907 +msgid "Running stitch length" +msgstr "" + +#: embroider.py:912 embroider.py:1008 +msgid "Zig-zag spacing (peak-to-peak)" +msgstr "" + +#: embroider.py:918 +msgid "Repeats" +msgstr "" + +#: embroider.py:999 +msgid "Custom satin column" +msgstr "" + +#: embroider.py:1014 +msgid "Pull compensation" +msgstr "" + +#: embroider.py:1022 +msgid "Contour underlay" +msgstr "" + +#: embroider.py:1022 embroider.py:1029 embroider.py:1034 +msgid "Contour Underlay" +msgstr "" + +#: embroider.py:1029 embroider.py:1047 +msgid "Stitch length" +msgstr "" + +#: embroider.py:1034 +msgid "Contour underlay inset amount" +msgstr "" + +#: embroider.py:1040 +msgid "Center-walk underlay" +msgstr "" + +#: embroider.py:1040 embroider.py:1047 +msgid "Center-Walk Underlay" +msgstr "" + +#: embroider.py:1052 +msgid "Zig-zag underlay" +msgstr "" + +#: embroider.py:1052 embroider.py:1057 embroider.py:1062 +msgid "Zig-zag Underlay" +msgstr "" + +#: embroider.py:1057 +msgid "Zig-Zag spacing (peak-to-peak)" +msgstr "" + +#: embroider.py:1062 +msgid "Inset amount (default: half of contour underlay inset)" +msgstr "" + +#: embroider.py:1108 +msgid "" +"One or more rails crosses itself, and this is not allowed. Please split " +"into multiple satin columns." +msgstr "" + +#: embroider.py:1115 +msgid "satin column: One or more of the rungs doesn't intersect both rails." +msgstr "" + +#: embroider.py:1115 embroider.py:1117 +msgid "Each rail should intersect both rungs once." +msgstr "" + +#: embroider.py:1117 +msgid "" +"satin column: One or more of the rungs intersects the rails more than once." +msgstr "" + +#: embroider.py:1158 +#, python-format +msgid "satin column: object %s has a fill (but should not)" +msgstr "" + +#: embroider.py:1162 +#, python-format +msgid "" +"satin column: object %(id)s has two paths with an unequal number of points " +"(%(length1)d and %(length2)d)" +msgstr "" + +#: embroider.py:1695 +msgid "" +"\n" +"\n" +"Seeing a 'no such option' message? Please restart Inkscape to fix." +msgstr "" + +#: embroider.py:1743 +msgid "No embroiderable paths selected." +msgstr "" + +#: embroider.py:1745 +msgid "No embroiderable paths found in document." +msgstr "" + +#: embroider.py:1746 +msgid "" +"Tip: use Path -> Object to Path to convert non-paths before embroidering." +msgstr "" + +#: embroider.py:1758 +msgid "Embroidery" +msgstr "" + +#: embroider_params.py:243 +msgid "These settings will be applied to 1 object." +msgstr "" + +#: embroider_params.py:245 +#, python-format +msgid "These settings will be applied to %d objects." +msgstr "" + +#: embroider_params.py:248 +msgid "" +"Some settings had different values across objects. Select a value from the " +"dropdown or enter a new one." +msgstr "" + +#: embroider_params.py:252 +#, python-format +msgid "Disabling this tab will disable the following %d tabs." +msgstr "" + +#: embroider_params.py:254 +msgid "Disabling this tab will disable the following tab." +msgstr "" + +#: embroider_params.py:257 +#, python-format +msgid "Enabling this tab will disable %s and vice-versa." +msgstr "" + +#: embroider_params.py:287 +msgid "Inkscape objects" +msgstr "" + +#: embroider_params.py:345 +msgid "Embroidery Params" +msgstr "" + +#: embroider_params.py:359 +msgid "Presets" +msgstr "" + +#: embroider_params.py:364 +msgid "Load" +msgstr "" + +#: embroider_params.py:367 +msgid "Add" +msgstr "" + +#: embroider_params.py:370 +msgid "Overwrite" +msgstr "" + +#: embroider_params.py:373 +msgid "Delete" +msgstr "" + +#: embroider_params.py:376 +msgid "Cancel" +msgstr "" + +#: embroider_params.py:380 +msgid "Use Last Settings" +msgstr "" + +#: embroider_params.py:383 +msgid "Apply and Quit" +msgstr "" + +#: embroider_params.py:428 +msgid "Preview" +msgstr "" + +#: embroider_params.py:446 +msgid "Internal Error" +msgstr "" + +#: embroider_params.py:499 +msgid "Please enter or select a preset name first." +msgstr "" + +#: embroider_params.py:499 embroider_params.py:505 embroider_params.py:533 +msgid "Preset" +msgstr "" + +#: embroider_params.py:505 +#, python-format +msgid "Preset \"%s\" not found." +msgstr "" + +#: embroider_params.py:533 +#, python-format +msgid "" +"Preset \"%s\" already exists. Please use another name or press \"Overwrite\"" +msgstr "" + +#: embroider_simulate.py:303 +msgid "Embroidery Simulation" +msgstr "" + +#: inkstitch.py:94 +#, python-format +msgid "parseLengthWithUnits: unknown unit %s" +msgstr "" + +#: inkstitch.py:118 +#, python-format +msgid "Unknown unit: %s" +msgstr "" + +#: inkstitch.py:321 +msgid "TRIM after" +msgstr "" + +#: inkstitch.py:322 +msgid "Trim thread after this object (for supported machines and file formats)" +msgstr "" + +#: inkstitch.py:331 +msgid "STOP after" +msgstr "" + +#: inkstitch.py:332 +msgid "" +"Add STOP instruction after this object (for supported machines and file " +"formats)" +msgstr "" diff --git a/translations/.keep b/translations/.keep new file mode 100644 index 000000000..e69de29bb