@ -0,0 +1,50 @@ |
|||||
|
|
||||
|
The software development world converged on the **Queen Elizabeth II Centre** in Westminster from **January 26-30** for **NDC London 2026**. As one of the most anticipated tech conferences in Europe, this year’s event delivered a masterclass in the future of the stack. |
||||
|
|
||||
|
We have spent five days immersed in workshops and sessions. Here is our comprehensive recap of the highlights and the technical shifts that will define 2026\. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## **1\. High-Performance .NET and C\# Evolution** |
||||
|
|
||||
|
A major focus this year was the continued evolution of the .NET ecosystem. Experts delivered standout sessions on high-performance coding patterns, it’s clear that efficiency and "Native AOT" (Ahead-of-Time compilation) are no longer niche topics, they are becoming industry standards. |
||||
|
|
||||
|
## **1\. Moving Beyond the AI Hype** |
||||
|
|
||||
|
If 2025 was about experimenting with LLMs, NDC London 2026 was about AI integration. Sessions from experts showcased how developers are moving past simple chatbots and integrating AI directly into the CI/CD pipeline and automated testing suites. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## **3\. The "Hallway Track" and Community Networking** |
||||
|
|
||||
|
One of the biggest draws of **NDC London** is the community. Between the 100+ sessions, the exhibitor hall was buzzing with live demos and networking. |
||||
|
|
||||
|
Watch the video: |
||||
|
|
||||
|
[](https://www.youtube.com/watch?v=yb-FILkqL7U) |
||||
|
|
||||
|
 |
||||
|
 |
||||
|
|
||||
|
## **4\. The Big Giveaway: Our Xbox Series S Raffle** |
||||
|
|
||||
|
One of our favorite moments of the week was our Raffle Session. We love giving back to the community that inspires us, and this year, the energy at our booth was higher than ever. |
||||
|
|
||||
|
We were thrilled to give away a brand-new Xbox Series S to one lucky winner\! It was fantastic to meet so many of you who stopped by to enter, chat about your current projects, and share your thoughts on the future of the industry. |
||||
|
|
||||
|
**Congratulations again to our 2026 winner\!** We hope you enjoy some well-deserved gaming time after a long week of learning. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Watch the video: |
||||
|
|
||||
|
[](https://www.youtube.com/watch?v=W5HRwys8dpE) |
||||
|
|
||||
|
|
||||
|
## **Final Thoughts: See You at NDC London 2027\!** |
||||
|
|
||||
|
NDC London 2026 proved once again why it is a cornerstone event for the global developer community. We are returning to our projects with a refreshed roadmap and a deeper understanding of the tools shaping our industry. |
||||
|
|
||||
|
 |
||||
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
@ -0,0 +1,239 @@ |
|||||
|
# What NDC London 2026 Looked Like From a Developer’s Perspective |
||||
|
|
||||
|
 |
||||
|
|
||||
|
This year we attended NDC London as a sponsor for [ABP](https://abp.io). The conference was held at the same place [Queen Elizabeth II](https://qeiicentre.london/). I guess this is the best conf for .NET developers around the world (thanks to the NDC team). It was 3 full days started from 28 to 30 January 2026. As exhibitor we talked a lot with the attendees who stopped by our booth or while we were eating or in the conf rooms. |
||||
|
|
||||
|
This is the best opportunity to know what everyone is doing in software society. While I was explaining ABP to the people who first time heard, I also ask about what they do in their work. Developers mostly work on web platforms. And as you know there's an AI transformation in our sector. That's why I wonder if other people also stick to the latest AI trend! Well... not as I expected. In Volosoft, we are tightly following AI trends, using in our daily development, injecting this new technology to our product and trying to benefit this as much as possible. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
This new AI trend is same as the invention of printing (by Johannes Gutenberg in 1450) or it's similar to invention of calculators (by William S. Burroughs in 1886). The countries who benefit these inventions got a huge increase in their welfare level. So, we welcome this new AI invention in software development, design, devops and testing. I also see this as a big wave in the ocean, if you are prepared and developed your skills, you can play with it 🌊 and it's called surfing or you'll die in the ocean. But not all the companies react this transformation quickly. Many developers use it like ChatGpt conversation (copy-paste from it) or using GitHub Co-Pilot in a limited manner. But as I heard from Steven Sanderson's session and other Microsoft employees, they are already using it to reproduce the bugs reported in the issues or creating even feature PRs via Co-Pilot. That's a good news for me! |
||||
|
|
||||
|
Here're some pictures from the conf and that's me on the left side with brown shoes :) |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Another thing I see, there's a decrease in the number of attendees'. I don't know the real reason but probably the IT companies cut the budget for conferences. As you also hear, many companies layoff because of the AI replaces some of the positions. |
||||
|
|
||||
|
The food was great during the conference. It was more like eating sessions for me. Lots of good meals from different countries' kitchen. In the second day, there was a party. People grabbed their beers, wines, beverages and made some more networking. |
||||
|
|
||||
|
I was expecting more AI oriented sessions but it was less then my expectations. Even though I was an exhibitor, I tried to attend some of the session. I'll tell you my notes about these. |
||||
|
|
||||
|
## Sessions / Talks |
||||
|
|
||||
|
### The dangers of probably-working software | Damian Brady |
||||
|
|
||||
|
The first session and keynote was from Damian Brady. He's part of Developer Advocacy team at GitHub. And the topic was "The dangers of probably-working software". He started with some negative impact of how generative AI is killing software, and he ended like this a not so bad, we can benefit from the AI transformation. First time I hear "sleepwalking" term for the development. He was telling when we generate code via AI, and we don't review well-enough, we're sleepwalkers. And that's correct! and good analogy for that case. This talk centers on a powerful lesson: *“**Don’t ship code you don’t truly understand.**”* |
||||
|
Damian tells a personal story from his early .NET days when he implemented a **Huffman compression algorithm** based largely on Wikipedia. The code **“worked” in small tests** but **failed in production**. The experience forced him to deeply understand the algorithm rather than relying on copied solutions. Through this story, he explores themes of trust, complexity, testing, and mental models in software engineering. |
||||
|
|
||||
|
#### What I learnt from this session |
||||
|
|
||||
|
- “It seems to work” is not the same as “I understand it.” |
||||
|
- Code copied from Wikipedia or StackOverflow is inherently risky in production. |
||||
|
- Passing tests on small datasets does not guarantee real-world reliability. |
||||
|
- Performance issues often surface only in edge cases. |
||||
|
- Delivery pressure can discourage deep understanding — to the detriment of quality. |
||||
|
- Always ask: “**When does this fail?**” — not just “**Why does this work?**” |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Playing the long game | Sheena O'Connell |
||||
|
|
||||
|
Sheena is a former software engineer who now trains and supports tech educators. She talks about AI tools... |
||||
|
AI tools are everywhere but poorly understood; there’s hype, risks, and mixed results. The key question is how individuals and organisations should play the long game so skilled human engineers—especially juniors—can still grow and thrive. |
||||
|
She showed some statistics about how job postings on Indeed platform dramatically decreasing for software developers. About AI generated-code, she tells, it's less secure, there might be logical problems or interesting bugs, human might not read code very well and understanding/debugging code might sometimes take much longer time. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
Being an engineer is about much more than a job title — it requires systems thinking, clear communication, dealing with uncertainty, continuous learning, discipline, and good knowledge management. The job market is shifting: demand for AI-skilled workers is rising quickly and paying premiums, and required skills are changing faster in AI-exposed roles. There’s strength in using a diversity of models instead of locking into one provider, and guardrails improve reliability. |
||||
|
|
||||
|
AI is creating new roles (like AI security, observability, and operations) and new kinds of work, while routine attrition also opens opportunities. At the same time, heavy AI use can have negative cognitive effects: people may think less, feel lonelier, and prefer talking to AI over humans. |
||||
|
|
||||
|
Organizations are becoming more dynamic and project-based, with shorter planning cycles, higher trust, and more experimentation — but also risk of “shiny new toy” syndrome. Research shows AI can boost productivity by 15–20% in many cases, especially in simpler, greenfield projects and popular languages, but it can actually reduce productivity on very complex work. Overall, the recommendation is to focus on using AI well (not just the newest model), add monitoring and guardrails, keep flexibility, and build tools that allow safe experimentation. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
|
||||
|
We’re in a messy, fast-moving AI era where LLM tools are everywhere but poorly understood. There’s a lot of hype and marketing noise, making it hard even for technical people to separate reality from fantasy. Different archetypes have emerged — from AI-optimists to skeptics — and both extremes have risks. AI is great for quick prototyping but unreliable for complex work, so teams need guardrails, better practices, and a focus on learning rather than “writing more code faster.” The key question is how individuals and organizations can play the long game so strong human engineers — especially juniors — can still grow and thrive in an AI-driven world. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Crafting Intelligent Agents with Context Engineering | Carly Richmond |
||||
|
|
||||
|
Carly is a Developer Advocate Lead at Elastic in London with deep experience in web development and agile delivery from her years in investment banking. A practical UI engineer. She brings a clear, hands-on perspective to building real-world AI systems. In her talk on **“Crafting Intelligent Agents with Context Engineering,”** she argues that prompt engineering isn’t enough — and shows how carefully shaping context across data, tools, and systems is key to creating reliable, useful AI agents. She mentioned about the context of an AI process. The context consists of Instructions, Short Memory, Long Memory, RAG, User Prompts, Tools, Structured Output. |
||||
|
|
||||
|
 |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Modular Monoliths | Kevlin Henney |
||||
|
|
||||
|
Kevlin frames the “microservices vs monolith” debate as a false dichotomy. His core argument is simple but powerful: problems rarely come from *being a monolith* — they come from being a **poorly structured one**. Modularity is not a deployment choice; it is an architectural discipline. |
||||
|
|
||||
|
## **Notes from the Talk** |
||||
|
|
||||
|
- A monolith is not inherently bad; a tangled monolith is. |
||||
|
- Architecture is mostly about **boundaries**, not boxes. |
||||
|
- If you cannot draw clean internal boundaries, you are not ready for microservices. |
||||
|
- Dependencies reveal your real architecture better than diagrams. |
||||
|
- Teams shape systems more than tools do (a modern reading of Conway’s Law). |
||||
|
- Splitting systems prematurely increases complexity without increasing clarity. |
||||
|
- Good modular design makes systems **easier to change, not just easier to scale**. |
||||
|
|
||||
|
## **Lessons for Developers** |
||||
|
|
||||
|
- Start with a well-structured modular monolith before considering microservices. |
||||
|
- Treat modules as real first-class citizens: clear ownership, clear contracts. |
||||
|
- Make dependency direction explicit — no circular graphs. |
||||
|
- Use internal architectural tests to prevent boundary violations. |
||||
|
- Organize code by *capability*, not by technical layer. |
||||
|
- Optimize for **cognitive load**, not deployment topology. |
||||
|
- If your team structure is messy, your architecture will be messy — fix people, not tech. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### AI Coding Agents & Skills | Steve Sanderson |
||||
|
|
||||
|
 |
||||
|
|
||||
|
In this session, Steve started how Microsoft is excessively using AI tools for PRs, reproducing bug reports etc... He says, we use brains and hands less then anytime. And he summarized the AI assisted development into 10 outlines. These are Subagents, Plan Mode, Skills, Delegate, Memories, Hooks, MCP, Infinite Sessions, Plugins and Git Workflow. Let's see his ideas for each of these headings: |
||||
|
|
||||
|
## **1. Subagents** |
||||
|
|
||||
|
- Break big problems into smaller, specialized agents. |
||||
|
- Each subagent should have a clear responsibility and limited scope. |
||||
|
- Parallel work is better than one “smart but slow” agent. |
||||
|
- Reduces hallucination by narrowing context per agent. |
||||
|
- Easier to debug: you can inspect each agent’s output separately. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **2. Plan Mode** |
||||
|
|
||||
|
- Always start with a plan before generating code. |
||||
|
- The plan should be explicit, human-readable, and reviewable. |
||||
|
- Helps align expectations between you and the AI. |
||||
|
- Prevents wasted effort on wrong directions. |
||||
|
- Encourages structured thinking instead of trial-and-error coding. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **3. Skills** |
||||
|
|
||||
|
- Skills are reusable capabilities for AI agents. |
||||
|
- Treat skills like APIs: versioned, documented, and shareable. |
||||
|
- Prefer many small skills over one monolithic skill. |
||||
|
- Store skills in Git, not in chat history. |
||||
|
- Skills should integrate with real tools (CI, GitHub, browsers, etc.). |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **4. Delegate** |
||||
|
|
||||
|
- Don’t micromanage — delegate well-defined tasks. |
||||
|
- Give clear inputs, constraints, and success criteria. |
||||
|
- Let the AI own the implementation details. |
||||
|
- Review outcomes instead of every intermediate step. |
||||
|
- Use delegation for repetitive or mechanical work. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **5. Memories** |
||||
|
|
||||
|
- Long-term memory should capture decisions, not chat noise. |
||||
|
- Store *why* something was done, not every detail of *how*. |
||||
|
- Keep memory sparse and structured. |
||||
|
- Treat memory like documentation that evolves over time. |
||||
|
- Be careful about leaking sensitive data into persistent memory. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **6. Hooks** |
||||
|
|
||||
|
- Hooks connect AI actions to your real workflow. |
||||
|
- Examples: pre-commit checks, PR reviews, test triggers. |
||||
|
- Hooks make AI proactive instead of reactive. |
||||
|
- They reduce manual context switching for developers. |
||||
|
- Best hooks are lightweight and predictable. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **7. MCP (Model Context Protocol)** |
||||
|
|
||||
|
- Standard way for models to talk to external tools. |
||||
|
- Enables safe, controlled access to systems (files, APIs, databases). |
||||
|
- Prevents random tool usage; everything is explicit. |
||||
|
- Encourages ecosystem of interoperable tools. |
||||
|
- Critical for production-grade AI assistants. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **8. Infinite Sessions** |
||||
|
|
||||
|
- AI should remember the “project context,” not just the last message. |
||||
|
- Reduces repetition and re-explaining. |
||||
|
- Enables deeper reasoning over time. |
||||
|
- Works best when combined with structured memory. |
||||
|
- Still requires periodic cleanup to avoid context bloat. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **9. Plugins** |
||||
|
|
||||
|
- Extend AI capabilities beyond core model features. |
||||
|
- Plugins should solve real workflow problems, not demos. |
||||
|
- Prefer composable plugins over custom hacks. |
||||
|
- Security matters — don’t give plugins unlimited access. |
||||
|
- Treat plugins like dependencies: review and maintain them. |
||||
|
|
||||
|
------ |
||||
|
|
||||
|
## **10. Git Workflow** |
||||
|
|
||||
|
- AI should operate inside your existing Git process. |
||||
|
- Generate small, focused commits — not giant changes. |
||||
|
- Use AI for PR descriptions and code reviews. |
||||
|
- Keep humans in the loop for design decisions. |
||||
|
- Branching strategy still matters; AI doesn’t replace it. |
||||
|
|
||||
|
**Lessons for Developers from Steve's Talk** |
||||
|
|
||||
|
- Coding agents work best when you treat them like programmable teammates, not autocomplete tools. |
||||
|
- “Skills” are the right abstraction for scaling AI assistants across a team. |
||||
|
- A skill is fundamentally a structured Markdown file + metadata + optional scripts/tools. |
||||
|
- Load **descriptions first, details later** — this keeps LLM context small and reliable. |
||||
|
- Treat skills like shared APIs: version them, review them, and store them in source control. |
||||
|
- Skills can be installed from Git repos (marketplaces), not just created locally. |
||||
|
- Slash commands make skills fast, explicit, and reproducible in daily workflow. |
||||
|
- Use skills to bridge AI ↔ real systems (e.g., GitHub Actions, Playwright, build status). |
||||
|
- Automation skills are most valuable when they handle end-to-end flows (browser + app + data). |
||||
|
- Let the agent *discover* the right skill rather than hard-coding every step. |
||||
|
- Prefer small, composable skills over one “god skill.” |
||||
|
- Skills reduce hallucination risk by constraining what the agent is allowed to do. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### My Personal Notes about AI |
||||
|
|
||||
|
- This is your code tech stack for a basic .NET project: |
||||
|
|
||||
|
- Assembly > MSIL > C# > ASP.NET Core > NuGet + NPM > Your Handmade Business Code |
||||
|
|
||||
|
When we ask a development to an AI assisted IDE, AI never starts from Assembly or even it's not writing an existing NPM package. It basically uses what's there on the market. So we know frameworks like ASP.NET Core, ABP will always be there after AI evolution. |
||||
|
|
||||
|
- Software engineer is not just writing correct syntax code to explain a program to computer. As an engineer you need to understand the requirements, design the problem, make proper decisions and fix the uncertainty. Asking AI the right questions is very critical these days. |
||||
|
|
||||
|
- Tesla cars already started to go autonomous. As a driver, you don't need to care about how the car is driven. You need to choose the right way to go in the shortest time without hussle. |
||||
|
|
||||
|
- Nowadays, **developers big new issue is Reviewing the AI generated-code.** In the future, developers who use AI, who inspect AI generated code well and who tells the AI exactly what's needed will be the most important topics. Others (who's typing only code) will be naturally eliminated. Invest your time for these topics. |
||||
|
|
||||
|
- We see that our brain is getting lazier, our coding muscles gets weaker day by day. Just like after calculator invention, we stopped calculate big numbers. We'll eventually forget coding. But maybe that's what it needs to be! |
||||
|
|
||||
|
- Also I don't think AI will replace developers. Think about washing machines. Since they came out, they still need humans to put the clothes in the machine, pick the best program, take out from the machine and iron. From now on, AI is our assistance in every aspect of our life from shopping, medical issues, learning to coding. Let's benefit from it. |
||||
|
|
||||
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using System.Text; |
||||
|
using System.Text.Json; |
||||
|
using JetBrains.Annotations; |
||||
|
using Swashbuckle.AspNetCore.SwaggerUI; |
||||
|
using Volo.Abp; |
||||
|
|
||||
|
namespace Microsoft.Extensions.DependencyInjection; |
||||
|
|
||||
|
public static class AbpSwaggerUIOptionsExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Sets the abp.appPath used by the Swagger UI scripts.
|
||||
|
/// </summary>
|
||||
|
/// <param name="options">The Swagger UI options.</param>
|
||||
|
/// <param name="appPath">The application base path.</param>
|
||||
|
public static void AbpAppPath([NotNull] this SwaggerUIOptions options, [NotNull] string appPath) |
||||
|
{ |
||||
|
Check.NotNull(options, nameof(options)); |
||||
|
Check.NotNull(appPath, nameof(appPath)); |
||||
|
|
||||
|
var normalizedAppPath = NormalizeAppPath(appPath); |
||||
|
options.HeadContent = BuildAppPathScript(normalizedAppPath, options.HeadContent ?? string.Empty); |
||||
|
} |
||||
|
|
||||
|
private static string NormalizeAppPath(string appPath) |
||||
|
{ |
||||
|
return string.IsNullOrWhiteSpace(appPath) |
||||
|
? "/" |
||||
|
: appPath.Trim().EnsureStartsWith('/').EnsureEndsWith('/'); |
||||
|
} |
||||
|
|
||||
|
private static string BuildAppPathScript(string normalizedAppPath, string headContent) |
||||
|
{ |
||||
|
var builder = new StringBuilder(headContent); |
||||
|
if (builder.Length > 0) |
||||
|
{ |
||||
|
builder.AppendLine(); |
||||
|
} |
||||
|
|
||||
|
builder.AppendLine("<script>"); |
||||
|
builder.AppendLine(" var abp = abp || {};"); |
||||
|
builder.AppendLine($" abp.appPath = {JsonSerializer.Serialize(normalizedAppPath)};"); |
||||
|
builder.AppendLine("</script>"); |
||||
|
return builder.ToString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Auditing; |
||||
|
using Volo.Abp.Domain.Entities.Auditing; |
||||
|
|
||||
|
namespace Volo.Abp.Auditing.App.Entities; |
||||
|
|
||||
|
public class AppEntityWithComplexProperty : FullAuditedAggregateRoot<Guid> |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public AppEntityContactInformation ContactInformation { get; set; } |
||||
|
|
||||
|
[DisableAuditing] |
||||
|
public AppEntityContactInformation DisabledContactInformation { get; set; } |
||||
|
|
||||
|
public AppEntityWithComplexProperty() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AppEntityWithComplexProperty(Guid id, string name) |
||||
|
: base(id) |
||||
|
{ |
||||
|
Name = name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class AppEntityContactInformation |
||||
|
{ |
||||
|
public string Street { get; set; } = string.Empty; |
||||
|
|
||||
|
public AppEntityContactLocation Location { get; set; } = new(); |
||||
|
} |
||||
|
|
||||
|
public class AppEntityContactLocation |
||||
|
{ |
||||
|
public string City { get; set; } = string.Empty; |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
import { Component, inject, OnInit } from '@angular/core'; |
||||
|
import { LocalizationPipe, UILocalizationService, SessionStateService } from '@abp/ng.core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { CardComponent, CardBodyComponent } from '@abp/ng.theme.shared'; |
||||
|
import { AsyncPipe } from '@angular/common'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-localization-test', |
||||
|
standalone: true, |
||||
|
imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent, AsyncPipe], |
||||
|
template: ` |
||||
|
<div class="container mt-5"> |
||||
|
<h2>Hybrid Localization Test</h2> |
||||
|
|
||||
|
<abp-card cardClass="mt-4"> |
||||
|
<abp-card-body> |
||||
|
<h5>Backend Localization (if available)</h5> |
||||
|
<p><strong>MyProjectName::Welcome:</strong> {{ 'MyProjectName::Welcome' | abpLocalization }}</p> |
||||
|
<p><strong>AbpAccount::Login:</strong> {{ 'AbpAccount::Login' | abpLocalization }}</p> |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
|
||||
|
<abp-card cardClass="mt-4"> |
||||
|
<abp-card-body> |
||||
|
<h5>UI Localization (from /assets/localization/{{ currentLanguage$ | async }}.json)</h5> |
||||
|
<p><strong>MyProjectName::CustomKey:</strong> {{ 'MyProjectName::CustomKey' | abpLocalization }}</p> |
||||
|
<p><strong>MyProjectName::TestMessage:</strong> {{ 'MyProjectName::TestMessage' | abpLocalization }}</p> |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
|
||||
|
<abp-card cardClass="mt-4"> |
||||
|
<abp-card-body> |
||||
|
<h5>UI Override (UI > Backend Priority)</h5> |
||||
|
<p><strong>AbpAccount::Login:</strong> {{ 'AbpAccount::Login' | abpLocalization }}</p> |
||||
|
<p class="text-muted">If backend has "Login", UI version should override it</p> |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
|
||||
|
<abp-card cardClass="mt-4"> |
||||
|
<abp-card-body> |
||||
|
<h5>Loaded UI Localizations</h5> |
||||
|
<pre>{{ loadedLocalizations | json }}</pre> |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
</div> |
||||
|
`,
|
||||
|
}) |
||||
|
export class LocalizationTestComponent implements OnInit { |
||||
|
private uiLocalizationService = inject(UILocalizationService); |
||||
|
private sessionState = inject(SessionStateService); |
||||
|
|
||||
|
loadedLocalizations: any = {}; |
||||
|
currentLanguage$ = this.sessionState.getLanguage$(); |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.loadedLocalizations = this.uiLocalizationService.getLoadedLocalizations(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"MyProjectName": { |
||||
|
"Welcome": "Welcome from UI (en.json)", |
||||
|
"CustomKey": "This is a UI-only localization", |
||||
|
"TestMessage": "UI localization is working!" |
||||
|
}, |
||||
|
"AbpAccount": { |
||||
|
"Login": "Sign In (UI Override)" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"MyProjectName": { |
||||
|
"Welcome": "UI'dan Hoş Geldiniz (tr.json)", |
||||
|
"CustomKey": "Bu sadece UI'da olan bir localization", |
||||
|
"TestMessage": "UI localization çalışıyor!" |
||||
|
}, |
||||
|
"AbpAccount": { |
||||
|
"Login": "Giriş Yap (UI Override)" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,119 @@ |
|||||
|
import { Injectable, inject } from '@angular/core'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { BehaviorSubject, distinctUntilChanged, switchMap, of } from 'rxjs'; |
||||
|
import { catchError, shareReplay, tap } from 'rxjs/operators'; |
||||
|
import { ABP } from '../models/common'; |
||||
|
import { LocalizationService } from './localization.service'; |
||||
|
import { SessionStateService } from './session-state.service'; |
||||
|
import { CORE_OPTIONS } from '../tokens/options.token'; |
||||
|
|
||||
|
export interface UILocalizationResource { |
||||
|
[resourceName: string]: Record<string, string>; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Service for managing UI localizations in ABP Angular applications. |
||||
|
* Automatically loads localization files based on selected language |
||||
|
* Merges with backend localizations (UI > Backend priority) |
||||
|
*/ |
||||
|
@Injectable({ providedIn: 'root' }) |
||||
|
export class UILocalizationService { |
||||
|
private http = inject(HttpClient); |
||||
|
private localizationService = inject(LocalizationService); |
||||
|
private sessionState = inject(SessionStateService); |
||||
|
private options = inject(CORE_OPTIONS); |
||||
|
|
||||
|
private loadedLocalizations$ = new BehaviorSubject<Record<string, UILocalizationResource>>({}); |
||||
|
|
||||
|
private currentLanguage$ = this.sessionState.getLanguage$(); |
||||
|
|
||||
|
constructor() { |
||||
|
const uiLocalization = this.options.uiLocalization; |
||||
|
if (uiLocalization?.enabled) { |
||||
|
this.subscribeToLanguageChanges(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private subscribeToLanguageChanges() { |
||||
|
this.currentLanguage$ |
||||
|
.pipe( |
||||
|
distinctUntilChanged(), |
||||
|
switchMap(culture => this.loadLocalizationFile(culture)) |
||||
|
) |
||||
|
.subscribe(); |
||||
|
} |
||||
|
|
||||
|
private loadLocalizationFile(culture: string) { |
||||
|
const config = this.options.uiLocalization; |
||||
|
if (!config?.enabled) return of(null); |
||||
|
|
||||
|
const basePath = config.basePath || '/assets/localization'; |
||||
|
const url = `${basePath}/${culture}.json`; |
||||
|
|
||||
|
return this.http.get<UILocalizationResource>(url).pipe( |
||||
|
catchError(() => { |
||||
|
// If file not found or error occurs, return null
|
||||
|
return of(null); |
||||
|
}), |
||||
|
tap(data => { |
||||
|
if (data) { |
||||
|
this.processLocalizationData(culture, data); |
||||
|
} |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private processLocalizationData(culture: string, data: UILocalizationResource) { |
||||
|
const abpFormat: ABP.Localization[] = [ |
||||
|
{ |
||||
|
culture, |
||||
|
resources: Object.entries(data).map(([resourceName, texts]) => ({ |
||||
|
resourceName, |
||||
|
texts, |
||||
|
})), |
||||
|
}, |
||||
|
]; |
||||
|
this.localizationService.addLocalization(abpFormat); |
||||
|
|
||||
|
const current = this.loadedLocalizations$.value; |
||||
|
current[culture] = data; |
||||
|
this.loadedLocalizations$.next(current); |
||||
|
} |
||||
|
|
||||
|
addAngularLocalizeLocalization( |
||||
|
culture: string, |
||||
|
resourceName: string, |
||||
|
translations: Record<string, string>, |
||||
|
): void { |
||||
|
const abpFormat: ABP.Localization[] = [ |
||||
|
{ |
||||
|
culture, |
||||
|
resources: [ |
||||
|
{ |
||||
|
resourceName, |
||||
|
texts: translations, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
this.localizationService.addLocalization(abpFormat); |
||||
|
|
||||
|
const current = this.loadedLocalizations$.value; |
||||
|
if (!current[culture]) { |
||||
|
current[culture] = {}; |
||||
|
} |
||||
|
if (!current[culture][resourceName]) { |
||||
|
current[culture][resourceName] = {}; |
||||
|
} |
||||
|
current[culture][resourceName] = { |
||||
|
...current[culture][resourceName], |
||||
|
...translations, |
||||
|
}; |
||||
|
this.loadedLocalizations$.next(current); |
||||
|
} |
||||
|
|
||||
|
getLoadedLocalizations(culture?: string): UILocalizationResource { |
||||
|
const lang = culture || this.sessionState.getLanguage(); |
||||
|
return this.loadedLocalizations$.value[lang] || {}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; |
||||
|
import { of, Subject, throwError } from 'rxjs'; |
||||
|
import { HttpClient } from '@angular/common/http'; |
||||
|
import { UILocalizationService } from '../services/ui-localization.service'; |
||||
|
import { LocalizationService } from '../services/localization.service'; |
||||
|
import { SessionStateService } from '../services/session-state.service'; |
||||
|
import { CORE_OPTIONS } from '../tokens/options.token'; |
||||
|
|
||||
|
describe('UILocalizationService', () => { |
||||
|
let spectator: SpectatorService<UILocalizationService>; |
||||
|
let service: UILocalizationService; |
||||
|
let language$: Subject<string>; |
||||
|
let httpGet: ReturnType<typeof vi.fn>; |
||||
|
let addLocalizationSpy: ReturnType<typeof vi.fn>; |
||||
|
|
||||
|
const createService = createServiceFactory({ |
||||
|
service: UILocalizationService, |
||||
|
mocks: [HttpClient, LocalizationService], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: SessionStateService, |
||||
|
useFactory: () => { |
||||
|
let currentLanguage = 'en'; |
||||
|
language$ = new Subject<string>(); |
||||
|
language$.subscribe(lang => { |
||||
|
currentLanguage = lang; |
||||
|
}); |
||||
|
return { |
||||
|
getLanguage: vi.fn(() => currentLanguage), |
||||
|
getLanguage$: vi.fn(() => language$.asObservable()), |
||||
|
}; |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
provide: CORE_OPTIONS, |
||||
|
useValue: { |
||||
|
uiLocalization: { |
||||
|
enabled: true, |
||||
|
basePath: '/assets/localization', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
spectator = createService(); |
||||
|
service = spectator.service; |
||||
|
const http = spectator.inject(HttpClient); |
||||
|
const localizationService = spectator.inject(LocalizationService); |
||||
|
httpGet = vi.fn(); |
||||
|
(http as any).get = httpGet; |
||||
|
addLocalizationSpy = vi.fn(); |
||||
|
(localizationService as any).addLocalization = addLocalizationSpy; |
||||
|
}); |
||||
|
|
||||
|
describe('when uiLocalization is enabled', () => { |
||||
|
it('should load localization file when language changes', () => { |
||||
|
const uiData = { MyApp: { Welcome: 'Welcome from UI' } }; |
||||
|
httpGet.mockReturnValue(of(uiData)); |
||||
|
|
||||
|
language$.next('en'); |
||||
|
|
||||
|
expect(httpGet).toHaveBeenCalledWith('/assets/localization/en.json'); |
||||
|
expect(addLocalizationSpy).toHaveBeenCalledWith([ |
||||
|
{ |
||||
|
culture: 'en', |
||||
|
resources: [{ resourceName: 'MyApp', texts: { Welcome: 'Welcome from UI' } }], |
||||
|
}, |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
it('should use default basePath when not provided', () => { |
||||
|
expect(httpGet).not.toHaveBeenCalled(); |
||||
|
httpGet.mockReturnValue(of({})); |
||||
|
language$.next('en'); |
||||
|
expect(httpGet).toHaveBeenCalledWith('/assets/localization/en.json'); |
||||
|
}); |
||||
|
|
||||
|
it('should not call addLocalization when file is missing (HTTP error)', () => { |
||||
|
httpGet.mockReturnValue(throwError(() => new Error('404'))); |
||||
|
|
||||
|
language$.next('fr'); |
||||
|
|
||||
|
expect(httpGet).toHaveBeenCalledWith('/assets/localization/fr.json'); |
||||
|
expect(addLocalizationSpy).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it('should cache loaded data in getLoadedLocalizations', () => { |
||||
|
const uiData = { AbpAccount: { Login: 'Sign In (UI)' } }; |
||||
|
httpGet.mockReturnValue(of(uiData)); |
||||
|
|
||||
|
language$.next('en'); |
||||
|
|
||||
|
const loaded = service.getLoadedLocalizations('en'); |
||||
|
expect(loaded).toEqual(uiData); |
||||
|
}); |
||||
|
|
||||
|
it('should load again when language changes to another culture', () => { |
||||
|
httpGet.mockReturnValue(of({})); |
||||
|
language$.next('en'); |
||||
|
expect(httpGet).toHaveBeenCalledTimes(1); |
||||
|
|
||||
|
httpGet.mockClear(); |
||||
|
httpGet.mockReturnValue(of({ MyApp: { Title: 'Titre' } })); |
||||
|
language$.next('fr'); |
||||
|
|
||||
|
expect(httpGet).toHaveBeenCalledWith('/assets/localization/fr.json'); |
||||
|
expect(addLocalizationSpy).toHaveBeenCalledWith([ |
||||
|
{ |
||||
|
culture: 'fr', |
||||
|
resources: [{ resourceName: 'MyApp', texts: { Title: 'Titre' } }], |
||||
|
}, |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('addAngularLocalizeLocalization', () => { |
||||
|
it('should add localization via LocalizationService (UI data in merge pipeline)', () => { |
||||
|
service.addAngularLocalizeLocalization('en', 'MyApp', { |
||||
|
CustomKey: 'UI Override', |
||||
|
}); |
||||
|
|
||||
|
expect(addLocalizationSpy).toHaveBeenCalledWith([ |
||||
|
{ |
||||
|
culture: 'en', |
||||
|
resources: [ |
||||
|
{ |
||||
|
resourceName: 'MyApp', |
||||
|
texts: { CustomKey: 'UI Override' }, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]); |
||||
|
}); |
||||
|
|
||||
|
it('should merge into getLoadedLocalizations cache', () => { |
||||
|
service.addAngularLocalizeLocalization('en', 'MyApp', { Key1: 'Val1' }); |
||||
|
service.addAngularLocalizeLocalization('en', 'MyApp', { Key2: 'Val2' }); |
||||
|
|
||||
|
const loaded = service.getLoadedLocalizations('en'); |
||||
|
expect(loaded.MyApp).toEqual({ Key1: 'Val1', Key2: 'Val2' }); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('getLoadedLocalizations', () => { |
||||
|
it('should return empty object when no culture loaded', () => { |
||||
|
expect(service.getLoadedLocalizations('de')).toEqual({}); |
||||
|
}); |
||||
|
|
||||
|
it('should return current language when culture not passed', () => { |
||||
|
const uiData = { R: { K: 'V' } }; |
||||
|
httpGet.mockReturnValue(of(uiData)); |
||||
|
language$.next('tr'); |
||||
|
|
||||
|
const loaded = service.getLoadedLocalizations(); |
||||
|
expect(loaded).toEqual(uiData); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -1,44 +0,0 @@ |
|||||
// Karma configuration file, see link for more information |
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html |
|
||||
|
|
||||
module.exports = function (config) { |
|
||||
config.set({ |
|
||||
basePath: '', |
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'], |
|
||||
plugins: [ |
|
||||
require('karma-jasmine'), |
|
||||
require('karma-chrome-launcher'), |
|
||||
require('karma-jasmine-html-reporter'), |
|
||||
require('karma-coverage'), |
|
||||
require('@angular-devkit/build-angular/plugins/karma') |
|
||||
], |
|
||||
client: { |
|
||||
jasmine: { |
|
||||
// you can add configuration options for Jasmine here |
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html |
|
||||
// for example, you can disable the random execution with `random: false` |
|
||||
// or set a specific seed with `seed: 4321` |
|
||||
}, |
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser |
|
||||
}, |
|
||||
jasmineHtmlReporter: { |
|
||||
suppressAll: true // removes the duplicated traces |
|
||||
}, |
|
||||
coverageReporter: { |
|
||||
dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'), |
|
||||
subdir: '.', |
|
||||
reporters: [ |
|
||||
{ type: 'html' }, |
|
||||
{ type: 'text-summary' } |
|
||||
] |
|
||||
}, |
|
||||
reporters: ['progress', 'kjhtml'], |
|
||||
port: 9876, |
|
||||
colors: true, |
|
||||
logLevel: config.LOG_INFO, |
|
||||
autoWatch: true, |
|
||||
browsers: ['Chrome'], |
|
||||
singleRun: false, |
|
||||
restartOnFileChange: true |
|
||||
}); |
|
||||
}; |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
import 'zone.js'; |
||||
|
import 'zone.js/testing'; |
||||
|
import { getTestBed } from '@angular/core/testing'; |
||||
|
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; |
||||
|
|
||||
|
// Initialize Angular testing environment |
||||
|
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); |
||||
@ -1,26 +0,0 @@ |
|||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files |
|
||||
|
|
||||
import 'zone.js'; |
|
||||
import 'zone.js/testing'; |
|
||||
import { getTestBed } from '@angular/core/testing'; |
|
||||
import { |
|
||||
BrowserDynamicTestingModule, |
|
||||
platformBrowserDynamicTesting |
|
||||
} from '@angular/platform-browser-dynamic/testing'; |
|
||||
|
|
||||
declare const require: { |
|
||||
context(path: string, deep?: boolean, filter?: RegExp): { |
|
||||
keys(): string[]; |
|
||||
<T>(id: string): T; |
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
// First, initialize the Angular testing environment. |
|
||||
getTestBed().initTestEnvironment( |
|
||||
BrowserDynamicTestingModule, |
|
||||
platformBrowserDynamicTesting() |
|
||||
); |
|
||||
// Then we find all the tests. |
|
||||
const context = require.context('./', true, /\.spec\.ts$/); |
|
||||
// And load the modules. |
|
||||
context.keys().map(context); |
|
||||
@ -1,44 +0,0 @@ |
|||||
// Karma configuration file, see link for more information |
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html |
|
||||
|
|
||||
module.exports = function (config) { |
|
||||
config.set({ |
|
||||
basePath: '', |
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'], |
|
||||
plugins: [ |
|
||||
require('karma-jasmine'), |
|
||||
require('karma-chrome-launcher'), |
|
||||
require('karma-jasmine-html-reporter'), |
|
||||
require('karma-coverage'), |
|
||||
require('@angular-devkit/build-angular/plugins/karma') |
|
||||
], |
|
||||
client: { |
|
||||
jasmine: { |
|
||||
// you can add configuration options for Jasmine here |
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html |
|
||||
// for example, you can disable the random execution with `random: false` |
|
||||
// or set a specific seed with `seed: 4321` |
|
||||
}, |
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser |
|
||||
}, |
|
||||
jasmineHtmlReporter: { |
|
||||
suppressAll: true // removes the duplicated traces |
|
||||
}, |
|
||||
coverageReporter: { |
|
||||
dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'), |
|
||||
subdir: '.', |
|
||||
reporters: [ |
|
||||
{ type: 'html' }, |
|
||||
{ type: 'text-summary' } |
|
||||
] |
|
||||
}, |
|
||||
reporters: ['progress', 'kjhtml'], |
|
||||
port: 9876, |
|
||||
colors: true, |
|
||||
logLevel: config.LOG_INFO, |
|
||||
autoWatch: true, |
|
||||
browsers: ['Chrome'], |
|
||||
singleRun: false, |
|
||||
restartOnFileChange: true |
|
||||
}); |
|
||||
}; |
|
||||
@ -0,0 +1,7 @@ |
|||||
|
import 'zone.js'; |
||||
|
import 'zone.js/testing'; |
||||
|
import { getTestBed } from '@angular/core/testing'; |
||||
|
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; |
||||
|
|
||||
|
// Initialize Angular testing environment |
||||
|
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); |
||||
@ -1,26 +0,0 @@ |
|||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files |
|
||||
|
|
||||
import 'zone.js'; |
|
||||
import 'zone.js/testing'; |
|
||||
import { getTestBed } from '@angular/core/testing'; |
|
||||
import { |
|
||||
BrowserDynamicTestingModule, |
|
||||
platformBrowserDynamicTesting |
|
||||
} from '@angular/platform-browser-dynamic/testing'; |
|
||||
|
|
||||
declare const require: { |
|
||||
context(path: string, deep?: boolean, filter?: RegExp): { |
|
||||
keys(): string[]; |
|
||||
<T>(id: string): T; |
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
// First, initialize the Angular testing environment. |
|
||||
getTestBed().initTestEnvironment( |
|
||||
BrowserDynamicTestingModule, |
|
||||
platformBrowserDynamicTesting() |
|
||||
); |
|
||||
// Then we find all the tests. |
|
||||
const context = require.context('./', true, /\.spec\.ts$/); |
|
||||
// And load the modules. |
|
||||
context.keys().map(context); |
|
||||