Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
34
If you need some tasty CSS hamburger icons that animate in fantastic ways, here you go. Hamburgers is an MIT-licensed CSS library that gives you over a dozen beautifully animated navicons for use in your own projects. Also includes the Sass source so you can customize and compile it yourself. This is a beautiful little library!
over a year ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from A Beautiful Site

Revisiting FOUCE

It's been awhile since I wrote about FOUCE and I've since come up with an improved solution that I think is worth a post. This approach is similar to hiding the page content and then fading it in, but I've noticed it's far less distracting without the fade. It also adds a two second timeout to prevent network issues or latency from rendering an "empty" page. First, we'll add a class called reduce-fouce to the <html> element. <html class="reduce-fouce"> ... </html> Then we'll add this rule to the CSS. <style> html.reduce-fouce { opacity: 0; } </style> Finally, we'll wait until all the custom elements have loaded or two seconds have elapsed, whichever comes first, and we'll remove the class causing the content to show immediately. <script type="module"> await Promise.race([ // Load all custom elements Promise.allSettled([ customElements.whenDefined('my-button'), customElements.whenDefined('my-card'), customElements.whenDefined('my-rating') // ... ]), // Resolve after two seconds new Promise(resolve => setTimeout(resolve, 2000)) ]); // Remove the class, showing the page content document.documentElement.classList.remove('reduce-fouce'); </script> This approach seems to work especially well and won't end up "stranding" the user if network issues occur.

6 months ago 82 votes
If Edgar Allan Poe was into Design Systems

Once upon a midnight dreary, while I pondered, weak and weary, While I nodded, nearly napping, suddenly there came a tapping, "'Tis a design system," I muttered, "bringing order to the core— Ah, distinctly I remember, every button, every splendor, Each component, standardized, like a raven's watchful eyes, Unified in system's might, like patterns we restore— And each separate style injection, linked with careful introspection, 'Tis a design system, nothing more.

7 months ago 85 votes
Web Components Are Not the Future — They’re the Present

It’s disappointing that some of the most outspoken individuals against Web Components are framework maintainers. These individuals are, after all, in some of the best positions to provide valuable feedback. They have a lot of great ideas! Alas, there’s little incentive for them because standards evolve independently and don’t necessarily align with framework opinions. How could they? Opinions are one of the things that make frameworks unique. And therein lies the problem. If you’re convinced that your way is the best and only way, it’s natural to feel disenchanted when a decision is made that you don’t fully agree with. This is my open response to Ryan Carniato’s post from yesterday called “Web Components Are Not the Future.” WTF is a component anyway? # The word component is a loaded term, but I like to think of it in relation to interoperability. If I write a component in Framework A, I would like to be able to use it in Framework B, C, and D without having to rewrite it or include its entire framework. I don’t think many will disagree with that objective. We’re not there yet, but the road has been paved and instead of learning to drive on it, frameworks are building…different roads. Ryan states: If the sheer number of JavaScript frameworks is any indicator we are nowhere near reaching a consensus on how one should author components on the web. And even if we were a bit closer today we were nowhere near there a decade ago. The thing is, we don’t need to agree on how to write components, we just need to agree on the underlying implementation, then you can use classes, hooks, or whatever flavor you want to create them. Turns out, we have a very well-known, ubiquitous technology that we’ve chosen to do this with: HTML. But it also can have a negative effect. If too many assumptions are made it becomes harder to explore alternative space because everything gravitates around the establishment. What is more established than a web standard that can never change? If the concern is premature standardization, well, it’s a bit late for that. So let’s figure out how to get from where we are now to where we want to be. The solution isn’t to start over at the specification level, it’s to rethink how front end frameworks engage with current and emerging standards and work to improve them. Respectfully, it’s time to stop complaining, move on, and fix the things folks perceive as suboptimal. The definition of component # That said, we also need to realize that Web Components aren’t a 1:1 replacement for framework components. They’re tangentially related things, and I think a lot of confusion stems from this. We should really fix the definition of component. So the fundamental problem with Web Components is that they are built on Custom Elements. Elements !== Components. More specifically, Elements are a subset of Components. One could argue that every Element could be a Component but not all Components are Elements. To be fair, I’ve never really liked the term “Web Components” because it competes with the concept of framework components, but that’s what caught on and that's what most people are familiar with these days. Alas, there is a very important distinction here. Sure, a button and a text field can be components, but there are other types. For example, many frameworks support a concept of renderless components that exist in your code, but not in the final HTML. You can’t do that with Web Components, because every custom element results in an actual DOM element. (FWIW I don’t think this is a bad thing — but I digress…) As to why Web components don’t do all the things framework components do, that’s because they’re a lower level implementation of an interoperable element. They’re not trying to do everything framework components do. That’s what frameworks are for. It’s ok to be shiny # In fact, this is where frameworks excel. They let you go above and beyond what the platform can do on its own. I fully support this trial-and-error way of doing things. After all, it’s fun to explore new ideas and live on the bleeding edge. We got a lot of cool stuff from doing that. We got document.querySelector() from jQuery. CSS Custom Properties were inspired by Sass. Tagged template literals were inspired by JSX. Soon we’re getting signals from Preact. And from all the component-based frameworks that came before them, we got Web Components: custom HTML elements that can be authored in many different ways (because we know people like choices) and are fully interoperable (if frameworks and metaframeworks would continue to move towards the standard instead of protecting their own). Frameworks are a testbed for new ideas that may or may not work out. We all need to be OK with that. Even framework authors. Especially framework authors. More importantly, we all need to stop being salty when our way isn’t what makes it into the browser. There will always be a better way to do something, but none of us have the foresight to know what a perfect solution looks like right now. Hindsight is 20/20. As humans, we’re constantly striving to make things better. We’re really good at it, by the way. But we must have the discipline to reach various checkpoints to pause, reflect, and gather feedback before continuing. Even the cheapest cars on the road today will outperform the Model T in every way. I’m sure Ford could have made the original Model T way better if they had spent another decade working on it, but do you know made the next version even better than 10 more years? The feedback they got from actual users who bought them, sat in them, and drove them around on actual roads. Web Standards offer a promise of stability and we need to move forward to improve them together. Using one’s influence to rally users against the very platform you’ve built your success on is damaging to both the platform and the community. We need these incredible minds to be less divisive and more collaborative. The right direction # Imagine if we applied the same arguments against HTML early on. What if we never standardized it at all? Would the Web be a better place if every site required a specific browser? (Narrator: it wasn't.) Would it be better if every site was Flash or a Java applet? (Remember Silverlight? lol) Sure, there are often better alternatives for every use case, but we have to pick something that works for the majority, then we can iterate on it. Web Components are a huge step in the direction of standardization and we should all be excited about that. But the Web Component implementation isn’t compatible with existing frameworks, and therein lies an existential problem. Web Components are a threat to the peaceful, proprietary way of life for frameworks that have amassed millions of users — the majority of web developers. Because opinions vary so wildly, when a new standard emerges frameworks can’t often adapt to them without breaking changes. And breaking changes can be detrimental to a user base. Have you spotted the issue? You can’t possibly champion Web Standards when you’ve built a non-standard thing that will break if you align with the emerging standard. It’s easier to oppose the threat than to adapt to it. And of course Web Components don’t do everything a framework does. How can the platform possibly add all the features every framework added last week? That would be absolutely reckless. And no, the platform doesn’t move as fast as your framework and that’s sometimes painful. But it’s by design. This process is what gives us APIs that continue to work for decades. As users, we need to get over this hurdle and start thinking about how frameworks can adapt to current standards and how to evolve them as new ones emerge. Let’s identify shortcomings in the spec and work together to improve the ecosystem instead of arguing about who’s shit smells worse. Reinventing the wheel isn’t the answer. Lock-in isn’t the answer. This is why I believe that next generation of frameworks will converge on custom elements as an interoperable component model, enhance that model by sprinkling in awesome features of their own, and focus more on flavors (class-based, functional, signals, etc.) and higher level functionality. As for today's frameworks? How they adapt will determine how relevant they remain. Living dangerously # Ryan concludes: So in a sense there are nothing wrong with Web Components as they are only able to be what they are. It's the promise that they are something that they aren't which is so dangerous. The way their existence warps everything around them that puts the whole web at risk. It's a price everyone has to pay. So Web Components aren’t the specific vision you had for components. That's fine. But that's how it is. They're not Solid components. They’re not React components. They’re not Svelte components. They’re not Vue components. They’re standards-based Web Components that work in all of the above. And they could work even better in all of the above if all of the above were interested in advancing the platform instead of locking users in. I’m not a conspiracy theorist, but I find interesting the number of people who are and have been sponsored and/or hired by for-profit companies whose platforms rely heavily on said frameworks. Do you think it’s in their best interest to follow Web Standards if that means making their service less relevant and less lucrative? Of course not. If you’ve built an empire on top of something, there’s absolutely zero incentive to tear it down for the betterment of humanity. That’s not how capitalism works. It’s far more profitable to lock users in and keep them paying. But you know what…? Web Standards don't give a fuck about monetization. Longevity supersedes ingenuity # The last thing I’d like to talk about is this line here. Web Components possibly pose the biggest risk to the future of the web that I can see. Of course, this is from the perspective of a framework author, not from the people actually shipping and maintaining software built using these frameworks. And the people actually shipping software are the majority, but that’s not prestigious so they rarely get the high follower counts. The people actually shipping software are tired of framework churn. They're tired of shit they wrote last month being outdated already. They want stability. They want to know that the stuff they build today will work tomorrow. As history has proven, no framework can promise that. You know what framework I want to use? I want a framework that aligns with the platform, not one that replaces it. I want a framework that values incremental innovation over user lock-in. I want a framework that says it's OK to break things if it means making the Web a better place for everyone. Yes, that comes at a cost, but almost every good investment does, and I would argue that cost will be less expensive than learning a new framework and rebuilding buttons for the umpteenth time. The Web platform may not be perfect, but it continuously gets better. I don’t think frameworks are bad but, as a community, we need to recognize that a fundamental piece of the platform has changed and it's time to embrace the interoperable component model that Web Component APIs have given us…even if that means breaking things to get there. The component war is over.

9 months ago 83 votes
Component Machines

Components are like little machines. You build them once. Use them whenever you need them. Every now and then you open them up to oil them or replace a part, then you send them back to work. And work, they do. Little component machines just chugging along so you never have to write them from scratch ever again. Adapted from this tweet.

10 months ago 77 votes
Styling Custom Elements Without Reflecting Attributes

I've been struggling with the idea of reflecting attributes in custom elements and when it's appropriate. I think I've identified a gap in the platform, but I'm not sure exactly how we should fill it. I'll explain with an example. Let's say I want to make a simple badge component with primary, secondary, and tertiary variants. <my-badge variant="primary">foo</my-badge> <my-badge variant="secondary">bar</my-badge> <my-badge variant="tertiary">baz</my-badge> This is a simple component, but one that demonstrates the problem well. I want to style the badge based on the variant property, but sprouting attributes (which occurs as a result of reflecting a property back to an attribute) is largely considered a bad practice. A lot of web component libraries do it out of necessary to facilitate styling — including Shoelace — but is there a better way? The problem # I need to style the badge without relying on reflected attributes. This means I can't use :host([variant="..."]) because the attribute may or may not be set by the user. For example, if the component is rendered in a framework that sets properties instead of attributes, or if the property is set or changed programmatically, the attribute will be out of sync and my styles will be broken. So how can I style the badge based its variants without reflection? Let's assume we have the following internals, which is all we really need for the badge. <my-badge> #shadowRoot <slot></slot> </my-badge> What can we do about it? # I can't add classes to the slot, because :host(:has(.slot-class)) won't match. I can't set a data attribute on the host element, because that's the same as reflection and might cause issues with SSR and DOM morphing libraries. I could add a wrapper element around the slot and apply classes to it, but I'd prefer not to bloat the internals with additional elements. With a wrapper, users would have to use ::part(wrapper) to target it. Without the wrapper, they can set background, border, and other CSS properties directly on the host element which is more desirable. I could add custom states for each variant, but this gets messy for non-Boolean values and feels like an abuse of the API. Filling the gap # I'm not sure what the best solution is or could be, but one thing that comes to mind is a way to provide some kind of cross-root version of :has that works with :host. Something akin to: :host(:has-in-shadow-root(.some-selector)) { /* maybe one day… */ } If you have any thoughts on this one, hit me up on Twitter.

a year ago 75 votes

More in programming

Single-Use Disposable Applications

As search gets worse and “working code” gets cheaper, apps get easier to make from scratch than to find.

12 hours ago 3 votes
Thoughts on Motivation and My 40-Year Career

I’ve never published an essay quite like this. I’ve written about my life before, reams of stuff actually, because that’s how I process what I think, but never for public consumption. I’ve been pushing myself to write more lately because my co-authors and I have a whole fucking book to write between now and October. […]

7 hours ago 3 votes
Desktop UI frameworks written by a single person

Less known desktop UI frameworks Writing desktop software is hard. The UI technologies of Windows or MacOS are awful compared to web technology. What can trivially be done with HTML/CSS/JavaScript in few minutes can take hours using Windows’s win32 APIs or Mac’s Cocoa. That’s why the default technology for desktop apps, especially cross-platform, is Electron: a Chrome browser combined with Node runtime. The problem is that it’s bloaty: each app is a unique build of Chrome with a little bit of application code. Chrome is over 100MB so many apps ship less than 1MB of code in a 100M wrapper. People tried to address the problem of poor OS APIs by writing UI frameworks, often meant to be cross-platform. You’ve heard about QT, GTK, wxWindows. The problem with those is that they are also old, their APIs are not the greatest either and they are bloaty as well. There just doesn’t seem to be a good option. Writing your own framework seems impossible due to the size of task. But is it? I’ll show a couple of less-known UI frameworks written mostly be a single person, often done simply to enable writing an application. SWELL in WDL WDL is interesting. Justin Frankel, the guy who created Winamp, has a repository of C++ code he uses in different projects. After selling Winamp to AOL, a side quest of writing file sharing application, getting fired from AOL for writing file sharing application, he started a company building Reaper a digital audio workstation software for Windows. Winamp is a win32 API program and so is Reaper. At some point Justin decided to make a Mac version but by then he had a lot of code heavily using win32 APIs. So he did what anyone in his position would: he implemented win32 APIs for Mac OS and Linux and called it SWELL - Simple Windows Emulation Layer. Ok, actually no-one else would do it. It was an insane idea but it worked. It’s important to not over-state SWELL capabilities. It’s not Wine. You can’t take any win32 program and recompile for Mac with SWELL. Frankel is insanely pragmatic and so is his code. SWELL only implements the subset of APIs he uses in Reaper. At the same time Reaper is a big app so if SWELL works for Reaper, it could work for your app. WDL is open-source using permissive MIT license. Sublime Text For a few years Sublime Text was THE programmer’s editor. It was written by a single developer in C++ and he wrote a custom UI toolkit for it. Not open source but its existence shows it can be done. RAD Debugger RAD Debugger is an open-source Windows debugger for C/C++ apps written in C by mostly a single person. It implements a custom UI framework based on 3D renderer. The UI is integral part of the the app but the code is well structured so you probably can take just their UI / render code and use it in your own C / C++ app. Currently the app / UI is only for Windows but it’s designed to be cross-platform and they are working on porting the renderer to Mac OS / Linux. They use permissive MIT license and everything is written in C. Dear ImGUI Dear ImGui is a newer cross-platform, UI framework in C++. Open source, permissive MIT license. Written by mostly a single person. Ghostty Ghostty is a cross-platform terminal emulator and UI. It’s written in Zig by mostly a single person and uses it’s own low-level GPU renderer for the UI. You too can write your own UI framework At first the idea of writing your own UI framework seems impossibly daunting. What I’m hoping to show is that if you’re ambitious enough it’s possible to build cross platform desktop apps that are not just bloated 100MB Chrome wrappers around few kilobytes of custom code. I’m not saying it’s a simple thing, just that enough people did it that it’s possible. It shouldn’t be necessary but both Microsoft and Apple have tragically dropped the ball on providing decent, high-performance UI libraries for their OS. Microsoft even writes their own apps, like Teams, in web technologies. Thanks to open source you’re not at the staring line. You can just use Dear ImGUI or WDL’s SWELL. Or you can extract the UI code from RAD Debugger or Ghostty (if you write in Zig). Or you can look at how their implementation to speed up your own design and implementation.

yesterday 2 votes
Logic for Programmers Turns One

I released Logic for Programmers exactly one year ago today. It feels weird to celebrate the anniversary of something that isn't 1.0 yet, but software projects have a proud tradition of celebrating a dozen anniversaries before 1.0. I wanted to share about what's changed in the past year and the work for the next six+ months. The Road to 0.1 I had been noodling on the idea of a logic book since the pandemic. The first time I wrote about it on the newsletter was in 2021! Then I said that it would be done by June and would be "under 50 pages". The idea was to cover logic as a "soft skill" that helped you think about things like requirements and stuff. That version sucked. If you want to see how much it sucked, I put it up on Patreon. Then I slept on the next draft for three years. Then in 2024 a lot of business fell through and I had a lot of free time, so with the help of Saul Pwanson I rewrote the book. This time I emphasized breadth over depth, trying to cover a lot more techniques. I also decided to self-publish it instead of pitching it to a publisher. Not going the traditional route would mean I would be responsible for paying for editing, advertising, graphic design etc, but I hoped that would be compensated by much higher royalties. It also meant I could release the book in early access and use early sales to fund further improvements. So I wrote up a draft in Sphinx, compiled it to LaTeX, and uploaded the PDF to leanpub. That was in June 2024. Since then I kept to a monthly cadence of updates, missing once in November (short-notice contract) and once last month (Systems Distributed). The book's now on v0.10. What's changed? A LOT v0.1 was very obviously an alpha, and I have made a lot of improvements since then. For one, the book no longer looks like a Sphinx manual. Compare! Also, the content is very, very different. v0.1 was 19,000 words, v.10 is 31,000.1 This comes from new chapters on TLA+, constraint/SMT solving, logic programming, and major expansions to the existing chapters. Originally, "Simplifying Conditionals" was 600 words. Six hundred words! It almost fit in two pages! The chapter is now 2600 words, now covering condition lifting, quantifier manipulation, helper predicates, and set optimizations. All the other chapters have either gotten similar facelifts or are scheduled to get facelifts. The last big change is the addition of book assets. Originally you had to manually copy over all of the code to try it out, which is a problem when there are samples in eight distinct languages! Now there are ready-to-go examples for each chapter, with instructions on how to set up each programming environment. This is also nice because it gives me breaks from writing to code instead. How did the book do? Leanpub's all-time visualizations are terrible, so I'll just give the summary: 1180 copies sold, $18,241 in royalties. That's a lot of money for something that isn't fully out yet! By comparison, Practical TLA+ has made me less than half of that, despite selling over 5x as many books. Self-publishing was the right choice! In that time I've paid about $400 for the book cover (worth it) and maybe $800 in Leanpub's advertising service (probably not worth it). Right now that doesn't come close to making back the time investment, but I think it can get there post-release. I believe there's a lot more potential customers via marketing. I think post-release 10k copies sold is within reach. Where is the book going? The main content work is rewrites: many of the chapters have not meaningfully changed since 1.0, so I am going through and rewriting them from scratch. So far four of the ten chapters have been rewritten. My (admittedly ambitious) goal is to rewrite three of them by the end of this month and another three by the end of next. I also want to do final passes on the rewritten chapters; as most of them have a few TODOs left lying around. (Also somehow in starting this newsletter and publishing it I realized that one of the chapters might be better split into two chapters, so there could well-be a tenth technique in v0.11 or v0.12!) After that, I will pass it to a copy editor while I work on improving the layout, making images, and indexing. I want to have something worthy of printing on a dead tree by 1.0. In terms of timelines, I am very roughly estimating something like this: Summer: final big changes and rewrites Early Autumn: graphic design and copy editing Late Autumn: proofing, figuring out printing stuff Winter: final ebook and initial print releases of 1.0. (If you know a service that helps get self-published books "past the finish line", I'd love to hear about it! Preferably something that works for a fee, not part of royalties.) This timeline may be disrupted by official client work, like a new TLA+ contract or a conference invitation. Needless to say, I am incredibly excited to complete this book and share the final version with you all. This is a book I wished for years ago, a book I wrote because nobody else would. It fills a critical gap in software educational material, and someday soon I'll be able to put a copy on my bookshelf. It's exhilarating and terrifying and above all, satisfying. It's also 150 pages vs 50 pages, but admittedly this is partially because I made the book smaller with a larger font. ↩

2 days ago 4 votes
Implementing UI translation in SumatraPDF, a C++ Windows application

Translating user interface of SumatraPDF SumatraPDF is the best PDF/eBook/Comic Book viewer for Windows. It’s small, fast, full of features, free and open-source. It became popular enough that it made sense to translate the UI for non-English users. Currently we support 72 languages. This article describes how I designed and implemented a translation system in SumatraPDF, a native win32 C++ Windows application. Hard things about translating the UI There are 2 hard things about translating an application code for translation system (extracting strings to translate, translate strings from English to user’s language) translating them into many languages Extracting strings to translate from source code Currently there are 381 strings in SumatraPDF subject to translation. It’s important that the system requires the least amount of effort when adding new strings to translate. Every string that needs to be translated is marked in .cpp or .h file with one of two macros: _TRA("Rename") _TRN("Open") I have a script that extracts those strings from source files. Mine is written in Go but it could just as well be Python or JavaScript. It’s a simple regex job. _TR stands for “translation”. _TRA(s) expands into const char* trans::GetTranslation(const char* str) function which returns str translated to current UI language. We auto-detect language at startup based on Windows settings and allow the user to explicitly set UI language. For English we just return the original string. If a string to be translated is e.g. a part of const char* array[], we can’t use trans::GetTranslation(). For cases like that we have _TRN() which expands to English string. We have to write code to translate it at some point. Adding new strings is therefore as simple as wrapping them in _TRA() or _TRN() macros. Translating strings into many languages Now that we’ve extracted strings to be translated, we need to translate them into 72 languages. SumatraPDF is a free, open-source program. I don’t have a budget to hire translators. I don’t have a budget, period. The only option was to get help from SumatraPDF users. It was vital to make it very easy for users to send me translations. I didn’t want to ask them, for example, to download some translation software. Design and implementation of AppTranslator web app I couldn’t find a really simple software for crowd sourcing translations so I wrote my own: https://github.com/kjk/apptranslator You can see it in action: https://www.apptranslator.org/app/SumatraPDF I designed it to be generic but I don’t think anyone else is using it. AppTranslator is simple. Per https://tools.arslexis.io/wc/: 4k lines of Go server code 451 lines of html code a single dependency: bootstrap CSS framework (the project is old) It’s simple because I don’t want to spend a lot of time writing translation software. It’s just a side project in service of the goal of translating SumatraPDF. Login is exclusively via GitHub. It doesn’t even use a database. Like in Redis, changes are stored as a series of operations in an append-only log. We keep the whole state in memory and re-create it from the log at startup. Main operation is translate a string from English to language X represented as [kOpTranslation, english string, language, translation, user who provided translation]. When user provides a translation in the web UI, we send an API call to the server which appends the translation operation to the log. Simple and reliable. Because the code is written in Go, it’s very fast and memory efficient. When running it uses mere megabytes of RAM. It can comfortably run on the smallest 256 MB VPS server. I backup the log to S3 so if the server ever fails, I can re-install the program on a new server and re-download the translations from S3. I provide RSS feed for each language so that people who provide translations can monitor for new strings to be translated. Sending strings for translation and receiving translations So I have a web app for collecting translations and a script that extracts strings to be translated from source code. How do they connect? AppTranslator has an API for submitting the current set of strings to be translated in the simplest possible format: a line for each string (I ensure there are no newlines in the string itself by escaping them with \n) API is password protected because only I can submit the strings. The server compares the strings sent with the current set and records a difference in the log. It also sends a response with translations. Again the simplest possible format: AppTranslator: SumatraPDF 651b739d7fa110911f25563c933f42b1d37590f8 :%s annotation. Ctrl+click to edit. am:%s մեկնաբանություն: Ctrl+քլիք՝ խմբագրելու համար: ar:ملاحظة %s. اضغط Ctrl للتحرير. az:Qeyd %s. Düzəliş etmək üçün Ctrl+düyməyə basın. As you can see: a string to translate is on a line starting with : is followed by translations of that strings in the format: ${lang}: ${translation} An optimization: 651b739d7fa110911f25563c933f42b1d37590f8 is a hash of this response. If I submit this hash with my request and translations didn’t change on the server, the response is empty. Implementing C++ part of translation system So now I have a text file with translation downloaded from the server. How do I get a translation in my C++ code? As with everything in SumatraPDF, I try to do things in a simple and efficient way. The whole Translation.cpp is only 239 lines of code. The core of translation system is const char* trans::GetTranslation(const char* s); function. I embed the translations in exact the same format as received from AppTranslator in the executable as data file in resources. If the UI language is English, we do nothing. trans::GetTranslation() returns its argument. When we switch the language, we load the translations from resources and build an index: an array of English strings an array of corresponding translations Both arrays use my own StrVec class optimized for storing an array of strings. To find a translation we scan the first array to find an index of the string and return translation from the second array, at the same index. Linear scan seems like it would be slow but it isn’t. Resizing dialogs I have a few dialogs defined in SumatraPDF.rc file. The problem with dialogs is that position of UI elements is fixed. A translated string will almost certainly have a different size than the English string which will mess up fixed layout. Thankfully someone wrote DialogSizer that smartly resizes dialogs and solves this problem. The evolution of a solution No AppTranslator My initial implementation was simpler. I didn’t yet have AppTranslator so I stored the strings in a text file in repository in the same format as what I described above. People would download it, make changes using a text editor and send me the file via email which I would then checkin. It worked for a while but it became worse over time. More strings, more languages created more work for me to manually manage e-mail submissions. I decided to automate the process. Code generation My first implementation of C++ side used code generation instead of embedding the text file in resources. My Go script would generate C++ source code files with static const char* [] arrays. This worked well but I decided to improve it further by making the code use the text file with translations embedded in the app. The main motivation for the change was to open a possibility of downloading latest translations from the server to fix the problem of translations not being all ready when I build the release executable. I haven’t done that yet but it’s now easier to implement given that the format of strings embedded in the exe is the same as the one I can download from AppTranslator. Only utf-8 SumatraPDF started by using both WCHAR* Unicode strings and char* utf8 strings. For that reason the translation system had to support returning translation in both WCHAR* and char* version. Over time I refactored the code to use mostly utf8 and at some point I no longer needed to support WCHAR* version. That made the code even smaller and reduced memory usage. The experience I’m happy how things turned out. AppTranslator proved to be reliable and hassle free. It runs for many years now and collected 35440 string translations from users. I automated everything so that all I need to do is to periodically re-run the script that extracts strings from source code, uploads them to AppTranslator and downloads latest translations. One problem is that translations are not always ready in time for release so I make a release and then people start translating strings added since last release. I’ve considered downloading the latest translations from the server, in addition to embedding them in an executable at the time of building the app. Would I do the same today? While AppTranslator is reliable and doesn’t require on-going work, it would be better to not have to run a server at all. The world has changed since I started SumatraPDF. Namely: people are comfortable using GitHub and you can edit files directly in GitHub UI. It’s not a great experience but it works. One option would be to generate a translation text file for each language, in this format: :first untranslated string :second untranslated string :first translated string translation of first string :second translated string translation of second string Untranslated strings are listed at the top, to make it easier to find. A link would send a translator directly to edit this file in GitHub UI. When translator saves translations, it creates a PR for me to review and merge. The roads not taken But why did you re-invent everything? You should do X instead. All other X that I know about suck. Using per-language .rc resource files Traditional way of localizing / translating Window GUI apps is to store all strings and dialog definitions in an .rc file. Each language gets its own .rc file (or files) and the program picks the right resource based on a language. This doesn’t solve the 2 hard problems: having an easy way to add strings for translations having an easy way for users to provide translations XML horror show There was a dark time when the world was under the iron grip of XML fanaticism. Everything had to be an XML file even when it was the worst possible solution for the problem. XML doesn’t solve the 2 hard problems and a string storage format is an absolute nightmare for human editing. GNU gettext There’s a C library gettext that uses .po files. This is much saner solution than XML horror show. .po files are relatively simple text format. The code is already written. Warning: tooting my own horn. My format is better. It’s easier for people to edit, it’s easier to write code to parse it. This looks like many times more than 239 lines of code. Ok, gettext probably does a bit more than my code, but clearly nothing than I need. It also doesn’t solve the 2 hard problems. I would still have to write code to extract strings from source code and build a way to allow users to translate them easily.

2 days ago 3 votes