This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.
You know those version numbers in libraries and frameworks, right? If not, here's a quick refresher. When we have something like package@x.y.z:
- x is a major release, where breaking changes are allowed,
- y is a backward-compatible feature release,
- z is a patch, usually containing small fixes and bugfixes.
If we want to keep our projects healthy, we should update at least patch versions regularly. In fact, nowadays our package managers often do it automatically if we let them.
One day, however, upgrading just the third number completely broke our application. And it wasn't the framework's fault. It was ours.
I fix plenty of bugs in my daily job. But finding the one worth telling in a contest meant digging surprisingly far back into my memory...
So let me take you back to 2017.
Welcome Back to 2017
Web applications have only been taking over the world for a few years. ECMAScript 6 has been around for two years already, but we're still using its revolutionary features (Promises, arrow functions, let and const) very carefully because browser support isn't exactly great yet.
The market is so hungry for developers that companies are basically hiring anyone who can write:
export class
Meetups are full of talks like "Introduction to Angular." Well, the new Angular itself isn't even a year old yet. At least creating a new project only takes a single CLI command. Meanwhile, creating a React project still feels like downloading half of npm.
I'm working as a fairly ambitious mid-level developer in a team that has been using Angular almost since its very beginning.
The project is a large enterprise application used to monitor Fair Trade certified products. It has to work all over the world, from Western Europe to small African countries where someone might connect to the Internet from a public library once every few weeks over a painfully slow connection.
Future-proofing matters.
We're basically learning modern frontend development while building this application. How do Observables work? What exactly is RxJS? When should we use services?
It doesn't help that Angular itself is still maturing and sometimes things simply don't work because... well... there's a bug in Angular. We file issue after issue.
To Angular's credit, the team reacts incredibly fast. Sometimes they fix a bug before we even finish filling out the issue template.
Naturally, we keep upgrading regularly, including minor and patch releases.
The Problem With Angular's Early i18n
As I mentioned, our application has to support many languages. Angular already has the beginnings of an i18n system, but it's nothing like what we have today. Without diving too deeply into the implementation, translatable elements are simply marked with an i18n attribute:
<h1 i18n>Hello</h1>
The Angular CLI scans the application and generates translation files from those elements.
The problem is that each language requires its own build. Switching languages at runtime isn't really an option. And unfortunately, runtime language switching is a hard requirement for our application (I honestly don't remember why anymore 😄).
Since the Angular ecosystem is still incredibly young and there aren't any mature libraries solving this problem, we decide to build our own solution.
Our implementation is surprisingly simple. Whenever the language changes, we scan the page for every element containing the i18n attribute and replace its contents with the proper translation.
Elegant. Simple. Maybe fifteen lines of code.
The Patch Update
Everything works perfectly... until the middle of 2017.
Angular is now on version 4. (I remember they skipped version 3 for perfectly reasonable reasons, although I couldn't tell you what those reasons were anymore. 😄)
One day we perform a completely routine Angular upgrade. I don't remember the exact version numbers anymore, but it was something along the lines of upgrading from 4.2.4 to 4.2.8.
A tiny patch update. Nothing should happen. Except... it did. Our translations, our entire internationalization system. All gone!
The application starts normally and everything looks fine. But as soon as we try to switch languages... nothing happens. We're stuck with English.
Time for Some Digital Detective Work
Now we're entering my favorite part of programming: software forensics.
At first, I completely dismiss the idea that such a tiny Angular update could possibly be responsible. We upgrade patches all the time. Surely something else must have happened.
Back then, things like CI pipelines and automated testing are still in their infancy. Maybe our tester simply hasn't switched languages for a week or two?
I go through Git history. Nothing looks suspicious. None of the recent commits touch internationalization.
Maybe the backend is broken? Did the translation files disappear?
Nope. Everything is exactly where it should be.
Finally, almost unwillingly, I look at the Angular upgrade again.
I start going backwards. Patch after patch. And sure enough... between two tiny patch versions, translations suddenly stop working.
So Angular must be guilty. Or... is it?
Solving the Mystery
Digging through Angular's changelog, I found a change that basically said:
Why keep the unnecessary
i18nattribute in the generated HTML? The compiler has already done everything it needed with it. Let's remove it.
Suddenly everything made sense. Our entire runtime translation system depended on an implementation detail that Angular had never promised to preserve.
For Angular, removing that attribute was a perfectly reasonable cleanup. For us... it broke the entire application.
Official references (if you're curious). As you see, we weren't the only people affected 🤣:
- Angular changelog (Angular 4.2.6): "compiler: remove i18n markup even if no translations"
- PR #17999: https://github.com/angular/angular/pull/17999
- Original issue #11042: https://github.com/angular/angular/issues/11042
- Follow-up discussion #20055: https://github.com/angular/angular/issues/20055 (I even see my collegue's comments there 🥰)
The Fix
Fortunately, fixing the problem wasn't particularly difficult. It was just... annoying. Instead of relying on Angular's internal i18n attribute, we created our own directive.
Its name?
fi18n.
I know. Peak engineering creativity. 😂
Were We Terrible Engineers?
Does this story mean we were inexperienced developers who had no idea what we were doing?
Quite the opposite! We had successfully implemented runtime language switching years before "it was cool" 😉
The only mistake we made was assuming that something we happened to observe was actually part of Angular's public contract. It wasn't. We built our solution on an implementation detail that Angular had every right to change.
And eventually... it did.
Luckily for us, the application wasn't anywhere near production yet 😅. So in our youthful optimism, we got away with it.
Why This Bug?
After more than a decade of professional programming, why would I choose this bug instead of one of the many much bigger production incidents I've dealt with? Because the lesson has become even more relevant.
In 2026, frontend development looks completely different. Nobody gives talks called "Introduction to Angular" anymore. React, Angular and Vue are all mature ecosystems. Whatever problem you're facing, chances are someone has already solved it.
And then there's AI, which can already handle a surprising amount of routine programming work.
But could this still happen today? Absolutely. Just... probably not in frontend anymore.
Today we have a brand-new ecosystem growing at an incredible pace: AI, and especially AI agents.
This is where many of today's biggest engineering questions live. Conferences and meetups appear all the time because nobody has all the answers yet. We're still writing articles explaining how an agent loop works, and those aren't even beginner topics. Protocols like MCP evolve rapidly enough that staying current requires genuine effort.
It reminds me a lot of frontend development back in 2017.
And just like we did back then, it's incredibly easy to make dangerous assumptions during development.
Someone parses a model's free-form response with a regular expression instead of using structured output.
Someone assumes the model will always wrap JSON inside the exact same Markdown block.
Someone builds business logic around an undocumented field returned by a provider.
Someone assumes the exact same prompt will always trigger the exact same tool.
Everything works today. Tomorrow it probably still works. Next month, a tiny update changes one seemingly insignificant detail...
...and suddenly everything falls apart.
Just like our little i18n attribute did.
The Real Lesson
Looking back, I don't think this story is really about Angular. It's about something much more universal.
As engineers, we often mistake observable behavior for a guaranteed contract. Just because something exists today doesn't mean the authors intended you to rely on it.
If your solution depends on undocumented behavior, you're not building on solid ground. You've simply been lucky so far.
BUT!
That doesn't mean we should beat ourselves up over mistakes. Nobody gets everything right. The important part is learning from them.
And this particular project? It got a happy ending. I left it years ago. But the application is still alive and doing well. Most of my code has probably disappeared by now... but I checked just before writing this article.
The login screen still looks exactly the way I designed it almost a decade ago. That made me smile. HOW COOL IS THAT 🤣❤️
Top comments (42)
You're back at your absolute best! I read the whole article in one sitting… you have that special something that makes it impossible to stop reading once you've started. Heaven can wait, but not your article. 😉
And I think we've all run into this kind of bug, whether in frontend or backend development, after upgrading a framework or a language. I went through it when moving from PHP 7.4 to PHP 8.0, and then again from PHP 8.0 to PHP 8.1. What's fascinating is that, as stressful as these investigations can be, the process itself is incredibly engaging and exciting. And when you finally uncover the root cause, the feeling of satisfaction is hard to beat.
Aww, thank you so much, Pascal! ❤️ I actually wanted to write about something completely different yesterday, but I realized it would need much more research, and I was simply too tired for that. So I thought, "Why not write something lighter instead?" 😄
I rarely take part in these DEV contests, but I figured I must have a fun bug story somewhere. I sat down, and this one just flowed onto the page in a single sitting.
And I completely agree, tracking down a bug is incredibly satisfying once you finally find the root cause! Although in this particular case, the satisfaction was a bit smaller because the actual fix was... pretty annoying. 😂
That's actually one of the things I love about your writing style: even when it's "just" a bug story, it feels like a real journey rather than a technical post. The investigation is what keeps the reader hooked. 😉
And I know exactly what you mean about the fix being less satisfying than the discovery! Sometimes you spend hours following clues, testing hypotheses, and finally finding the root cause feels like solving a mystery… only to realize the actual fix is something painfully simple or annoying. 😂
But those are often the bugs we remember the longest — not because the solution was brilliant, but because the path to get there was.
Hahaha, that's exactly how bug fixing works! 😂 And honestly, I think that's true for software engineering in general. Finding the right solution is the exciting part. The implementation is often the easy bit that you can hand over to a junior developer... or these days, to AI. 😄
Exactly! That's why I've always seen programming as much more about investigation than implementation. Once you've found the right solution, writing the code is often the most straightforward part.
That's also why AI feels like such a natural tool for developers: it accelerates the implementation, but it doesn't replace the curiosity that leads you to the right solution in the first place. 😉
Exactly! 😄 And that's probably why we haven't seen thousands of million-dollar businesses built just by vibe-coding apps. 😂
The implementation is only one piece of the puzzle. The real value comes from understanding the problem, asking the right questions, and finding the right solution. AI can help you build much faster, but it can't replace that curiosity or judgment.
Exactly. Speed amplifies both good and bad decisions. If you've identified the right problem and the right solution, AI is an incredible accelerator. If not, it just helps you build the wrong thing much faster. 😄
Exactly! 😄 To be honest, I've never felt that coding speed was the main barrier preventing people from coming up with lots of bad business ideas. 😂
Haha, exactly! 😂 The world was never short of ideas — good ones or bad ones. The hard part has always been understanding whether a problem is real, whether people actually care about it, and whether you can reach the people who need the solution.
AI changes the cost of experimentation, which is huge. But it doesn't magically turn every idea into a business. Sometimes the hardest part isn't building the thing… it's figuring out what is worth building in the first place. 😉
How do you do it? I read the whole thing in one breath, such great storytelling!
This is an amazing Bug Smash and such a nostalgic story. 😄 So you were almost there at the birth of Angular as we know it today?
The part about the hungry market and
export classbrought back memories. What I remember from those days is that, at some companies, I wasn’t interviewed on a computer, I had to write code on paper. No Google, no compiler, just remember everything. And of course, the syntax had to be valid! 😂As for the fix, I’m just missing one piece: where was Stack Overflow in this story? 😂
And what a happy ending! Seeing part of your work still there after all these years is even cooler. It must feel like being an architect who returns years later and sees that their building is still standing.
Aww, thank you so much! ❤️ And yes! I always used to joke in job interviews that I had as many years of Angular experience as Angular itself. 😄
As for coding on paper... at my university we had to write C programs during exams on actual sheets of paper. 😂 And the professor somehow had a compiler built into his brain. He would look at your code for a second and confidently say, "This won't compile!" So yeah, I think all of Eastern Europe shared that experience. 🤣
And regarding Stack Overflow... unfortunately it wasn't very helpful during Angular's first months. There simply weren't many answers yet. You had to roam through GitHub issues like a wild animal, hoping someone had already run into the same problem. 😂
So you’re basically that mythical unicorn developer who can actually apply for those legendary HR job postings requiring more years of experience with a framework than the framework itself has existed? 😂
And this line just made my day: “You had to roam through GitHub issues like a wild animal.” 🤣
Hahaha, exactly! 😂 I always used to joke during interviews that I was that legendary candidate HR was looking for. 😄
It also shows how silly that requirement is. Angular 2 or 4 was a completely different framework from what we have today.
The CI angle looks unclaimed here, so: I don't think a test would have caught this, and I think that's the most useful thing in the story.
A test written in 2017 would have asserted that switching the language changes the text. It would have passed for exactly as long as the i18n attribute survived — because it was written against the same undocumented behavior the feature was. That isn't an independent witness. It's the same assumption with an assertion wrapped around it.
I had a smaller version of this recently. A pattern in my scanner was supposed to exclude a fixture directory, and my check verified that the pattern could recognize the folder. It never verified that the run which actually ships excluded it. Six known-good files were scored as real defects for weeks, and every check stayed green, because I had tested the predicate instead of the run.
The thing that would have caught yours is the thing you eventually built. fi18n makes the dependency yours, so it can't be removed by someone who never promised to keep it. The test that helps isn't "does translation work" — it's "does the attribute we depend on still exist," failing on 4.2.6 with a message naming exactly what disappeared.
Turning an implicit dependency into an explicit one you own is the whole fix. The good part is you got there without anyone telling you.
Thank you so much for such a thoughtful analysis! ❤️ Exactly. I think we'd catch something like this much faster today, which is one of the reasons it was so interesting to revisit such an old story.
It also highlights how important automated testing is. In this particular case, unit tests probably wouldn't have caught it anyway, because the i18n attributes were stripped only during the build process. An end-to-end test against a production build would have had a much better chance of exposing the issue.
The build-time detail is the whole thing, and I'd sharpen it: a test that runs before the build isn't weak against this class, it's blind to it. The defect only exists in an artifact that test never sees. That reframes it from "we should have had more tests" to "we had tests of the wrong artifact," which is a much less comfortable sentence and a more useful one.
I'd add one cheap thing alongside the e2e, because e2e against a production build is expensive enough that teams run it on a schedule, and a check that runs rarely decays quietly.
After the build, assert the compiled output directly for the thing you depend on. One line, runs every build, and it fails naming exactly what disappeared instead of surfacing as "translations broken" three steps downstream.
I got a small reminder of this yesterday. My build renames image assets with a content hash, and my own verification pattern didn't allow a hyphen inside that hash. It reported a missing image on a perfectly good deploy, and I was about to redeploy to fix nothing. I'd been asserting against the shape I assumed the build produced rather than the shape it actually produces.
The nice part of your fi18n fix is that the assertion becomes trivial afterward — once the attribute is yours, checking the build output for it is checking your own contract rather than someone else's leftovers.
That's a really interesting point, and I partly agree. 🙂 The only thing I'd push back on is that writing an assertion for the attribute already assumes you've considered the possibility that it might disappear.
Back then, that possibility simply never crossed our minds. 😄 This was a very different era. Angular was still in its infancy, and I suspect very few teams were running large production applications on it. We trusted that if the framework introduced an i18n attribute, it wouldn't silently remove it in a patch release.
Looking back, I think the only thing that would have reliably caught this without already knowing the answer would have been an end-to-end test checking the actual behavior: switch the language and verify that the translations change. That test doesn't care why it broke, only that the user-visible behavior is wrong.
Of course, a full production E2E suite is expensive and often runs only nightly (or even less frequently on very large projects). But a small set of smoke tests on every PR? I think that's probably the sweet spot.
You're right, and the pushback lands cleanly. My suggestion only works for someone who already knows which attribute to worry about. That's the same limit I keep hitting elsewhere: a check calibrated to a known failure is diagnostic and blind at the same time, and the specificity that makes it useful afterward is exactly what makes it unavailable beforehand. Yours is calibrated to an outcome, which is the only shape that covers causes nobody anticipated. I had it backwards for the case that matters — before you know.
Two things I'd add to your sweet spot, both about keeping it from quietly becoming the thing it replaced.
It has to run against the built artifact, not a dev server. That's cheap to get wrong, and a PR smoke suite pointed at dev reintroduces the exact gap you started with: everything green, in the one environment where the bug cannot exist.
And the assertions should stay at the user-visible layer even when a DOM-level one is easier to write. "Click the toggle, the visible text changes" survives a compiler cleanup. "The element carrying attribute X" is the original bug wearing a test's clothes.
Great bug story. These are the kinds of lessons that remind us why software maintenance is often harder than writing new features.
A small patch update can sometimes expose hidden assumptions in our codebase, and the real challenge is not just fixing the bug but understanding why it happened. Good testing, monitoring, and knowing our dependencies are just as important as writing code.
Stories like this are valuable for developers because they show the reality behind production systems. At codecan.net, we believe sharing these real-world experiences helps developers build better and more reliable software.
Thank you so much! ❤️ Exactly! And honestly... who hasn't broken an application with what was supposed to be a "simple" fix? 😂 Those are often the bugs that teach us the most, and make for the best stories years later.
Amazing write @sylwia-lask . I often wonder how engineers used to think and solve the problem before the advent of AI tools and your experience provides me a glimpse of that. Also the last line about learning from one's mistake is a helpful advice for engineers like us.
Hahaha, thank you so much! ❤️ Oh yes, those were great times. 😄 We didn't have AI to help us, so we spent a lot more time digging through documentation, GitHub issues, and experimenting until something finally clicked. It was slower, but you definitely learned a lot along the way.
We’ve all been there every once in a while 😂
We often underestimate the fact that routine changes can sometimes result in unexpected behavior, and sometimes even break an entire system.
I’m a year and a half in as a software engineer, but I’ve learned a lot of lessons along the way, including the importance of always checking SQL statements because if I’m missing a WHERE or a logical operator that is crucial for an operation, either I wipe the majority of the database or delete the entire thing.
That’s just me exaggerating but as someone who’s working a legacy PHP system, I have to be really careful not to introduce functions that are normally supported in modern PHP (>= 7.0.0). Fortunately it has not happened to me, and hopefully it doesn’t.
But there was one time at my workplace where one of my coworkers accidentally deployed their changes to the index file without reading the file name, and it contained the code for the login page.
At the time, I was leading the team, so I held him accountable and had him locate the backup. Luckily, he fixed it really quick before management or anyone else noticed.
Regardless, reading this piece was a huge refresher and a reminder that we’re human and we make careless mistakes. But recognizing them and learning from them is what allows us to grow 😄
Hahaha, oh yes! 😄 Legacy PHP can definitely provide a lot of... let's call it entertainment. 😂 Thanks so much for sharing your story and for the thoughtful comment! ❤️
We often mistake observable behavior for a guaranteed contract is a line that applies almost exactly to something I've been writing about with AI agents lately, an agent given broad repo access that behaves fine today because nobody's tested what happens when it reads untrusted content in a slightly different order. it works, until the one input nobody anticipated shows up and then it's exactly your i18n attribute story again: not a bug in the platform, but a bet on behavior nobody promised to keep.
the four someone assumes examples you listed (regex-parsing free-form output, assuming a consistent Markdown wrapper, building on an undocumented field, assuming stable tool-triggering) are basically a checklist of ways teams are quietly building on Angular-4.2.4-style implementation details right now, just with LLMs instead of a compiler. fi18n made me laugh out loud by the way incredible engineering creativity 😄
Thank you so much for this comment! ❤️ Yes, that's exactly the same pattern I see emerging in agentic engineering today. We're often building on behavior that happens to work instead of behavior that's actually guaranteed. It feels a lot like frontend development did back then.
And yes... fi18n was probably the peak of our engineering creativity. 😂 The project name happened to start with an "F", that's the whole secret behind it. 😄
Really cool story. And it still all comes down to better understanding of the system. In this case, holding onto proper documentation served its purpose—it helps you know what the system actually promises, rather than relying on what happens to work today. ✌️
Exactly! 😄 But those were pretty crazy times. The frontend ecosystem was still in its early days. A year later, we would have just used ngx-translate, and the whole thing would have worked out of the box.
That's actually one of the reasons I enjoy looking back at stories like this. AI agents feel a lot like frontend development did back in 2017. The ecosystem is still evolving rapidly, best practices are still emerging, and we're often building things ourselves that will probably become standard features a year from now.
Sylwia, I enjoyed how this started as an Angular story but ended with a lesson that feels just as relevant for AI today.
It's easy to build around behavior that "just happens to work" until one small update reminds us it was never part of the contract. Thanks for sharing 😀
Thank you so much, Hemapriya! ❤️ You're absolutely right, especially in the AI agent space, where everything is evolving at a crazy pace. Things change almost daily, so it's even more important to distinguish between what is actually guaranteed and what just happens to work today. 😄
The i18n bug itself is a great story, but the parallel to today's AI agent work is the part that stuck with me. "Someone assumes the exact same prompt will always trigger the exact same tool" is such a specific, real example of exactly this same mistake, mistaking observed behavior for a guaranteed contract, just with a different tech stack a decade later.
The fi18n naming made me laugh, but honestly that's the most relatable part of the whole post, the fix is never glamorous, it's just "stop depending on the thing you were never promised."
What I appreciate most is that you didn't frame this as "we were bad engineers." You built runtime language switching before it was standard, and the actual mistake was narrow and specific: relying on an implementation detail without realizing it wasn't part of the contract. That distinction matters, especially for anyone reading this who's currently building on some provider's undocumented JSON quirk and telling themselves it's fine because "it's worked for three months."
Thank you so much! ❤️ You summed it up beautifully, and that's exactly what I was hoping to convey with this article.
It was never meant to be a story about "bad engineering." It was about how easy it is to mistake observed behavior for a guaranteed contract, whether it's Angular in 2017 or AI agents today. The technology changes, but the pattern stays surprisingly similar.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.