Weeknote: 2024-W19

17 June 2024

For reasons that will become clear, I’ve fallen behind on my weeknotes. But, I want to get back in the saddle, so here we go. Catchup time. First up, week 19.

In week 18, I’d mostly finished the accessibility improvements that came out of the audit; there were just two big things left; marking up untranslated strings with the right lang attribute, and keyboard input for the 3d renderers.

Rewriting the entire 3D renderer

As mentioned in the last weeknote, Manyfold was using a single canvas and a scissor test to make sure the browser didn’t run out of WebGL contexts when showing lots of models at once. However, that meant that the individual canvases didn’t really exist, and so giving them keyboard input was a real pain.

The solution was to change back to individual canvases and move the rendering into an OffscreenCanvas and WebWorker. This should mean we have full control back again, and also make the app work better by moving the hard work of rendering off the main thread into the background.

This took me almost the entire week; refactoring the Typescript code for the existing renderer into a worker was a pain. Where should I split the code between the page and the worker? How do I need to change the classes I’d made? How on earth does worker communication work anyway?

There is an great big technical post in this bit of work which I should do at some point, and is what actually stopped me writing the weeknote when I was supposed to. But, this isn’t the place for it, so I’ll leave some highlights and move on:

  • WebWorker comms is extremely basic. You can pass very simple messages back and forth from the page to the worker, but that’s it. Not everything is cloneable for sending, and you basically have to implement your own protocol. That’s where I highly recommend Comlink, which basically abstracts away all that communication faff and gives you an RPC object interface to your worker, bringing you back to sensible OOP territory.
  • WebWorkers don’t have access to the DOM, that’s fair enough; but they also don’t have access to a bunch of standard methods that deal with the DOM. That makes sense, except some of those can be used for other things; In my case, the 3MF loader used DOMParser to parse XML. Taking inspiration from various GitHub issues, I had to modify the 3MF loader to work in the background; that’s now released as a separate loader you can pull in if you need it.

There’s a lot more, but you can look at the Pull Request if you’re interested in the excruciating detail. At the end of the week, I’d managed to get it all working again, and the app looked… exactly the same. Admittedly it was a lot smoother and faster, and you could control the models with the keyboard, but it didn’t feel like a lot of progress! But, it was a good reminder that good accessibility can drive improvement for everyone.

Tagging untranslated text

After that I had a little time left in the week, so I started attacking the “untranslated strings” problem (I wanted to set a lang attribute for them if it was different to the main page lang). It took a while to work out how Rails i18n was working under the hood, and I came up against a problem where the translation happened at a much deeper level than the rendering, making it difficult to know that a fallback translation was being used when putting the text actually into the page (where you’d want to set the attribute).

I solved this by doing something a bit hacky, but which seems to work. At some point I’ll clean this up and maybe push it upstream into Rails - it seems like something that the framework should take care of.

The solution was to:

  1. Make the core String type locale-aware; I extended the core class to add an attribute for it.
  2. Change Rails’ I18n::Backend::Fallbacks#translate to set that locale if a fallback translation was used.
  3. Once the string has bubbled back up out of the depths of the i18n system, I then changed the default t helper method to add a span tag with the appropriate attribute if the locale of a translated string was set.

In the end it wasn’t much code, but it was a bit tricky getting there. There’s far too much Rails code being replaced for my liking - if something changes in the original versions of these methods, mine will drift out of date. That’s why I need to get this pushed upstream - a proper PR will actually be quite clean. I’d also like a solution that doesn’t involve adding attributes to the core String class. Maybe a Rails PR will be the place to work out a better approach.

Next time…

I was going to do a full weeknote catchup in one go, but this is long enough already so I’ll break them up. Week 20 coming next, with adventures in usage tracking amongst other things!