npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@quadrel-enterprise-ui/language

v21.0.0

Published

Library for loading dynamic and static translation files

Readme

qd-language

TranslateLoader for @ngx-translate/core. Merges static and dynamic translation sources into one set of texts. The app decides which sources to use, and it can decide at runtime from backend config — no rebuild needed.

Coming from version 20? See the migration guide.

Setup

@NgModule({
  imports: [QdLanguageModule.forRoot()],
  providers: [provideHttpClient()]
})
export class AppModule {}
// The library loads the files, the app picks the language.
translate.setFallbackLang('en');
translate.use('en');

The two entry points

There is one implementation and two ways to reach it.

| | QdLanguageModule.forRoot(options) | provideQdLanguage(options) | | ------------------------------ | ----------------------------------- | ---------------------------- | | Use in | NgModule apps | standalone apps | | Registers | loader + sources | loader + sources — identical | | Also exports TranslateModule | yes | no |

forRoot() is a one-line wrapper: it calls provideQdLanguage() and passes the options straight through. There is no second code path, so the two cannot drift apart.

static forRoot(options: QdLanguageOptions = {}): ModuleWithProviders<QdLanguageModule> {
  return { ngModule: QdLanguageModule, providers: [provideQdLanguage(options)] };
}

The one difference is the TranslateModule export. Components declared in the module that imports QdLanguageModule get the translate pipe from it. With provideQdLanguage() every module imports TranslateModule itself.

This applies to feature modules either way. QdLanguageModule only covers the module that imports it — usually AppModule. Every feature module still needs its own TranslateModule import, otherwise its templates show raw keys although the files loaded.

Static sources

Files shipped with the app, one per language. They are always loaded, in every stage.

1. Default — nothing to configure

Loads app texts from ./assets/i18n/{lang}.json and framework texts from ./assets/i18n/framework/{lang}.json.

@NgModule({
  imports: [QdLanguageModule.forRoot()],
  providers: [provideHttpClient()]
})
export class AppModule {}

2. Own folders

Setting static replaces the default list, so list every folder you want — including the framework one. Order matters: later folders win, so keep the framework first. The last entry shows that suffix defaults to .json.

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      static: [
        { prefix: './assets/i18n/framework/', suffix: '.json' },
        { prefix: './assets/i18n/', suffix: '.json' },
        { prefix: './assets/i18n/legal/' }
      ]
    })
  ]
})
export class AppModule {}

3. Folders decided by backend config

A function instead of a list. It runs once, on the first translation load, which is always after APP_INITIALIZER — so backend config is available. It may use inject().

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      static: () => {
        const config = inject(MyAppConfigService);

        return [
          { prefix: './assets/i18n/framework/' },
          { prefix: './assets/i18n/' },
          ...(config.betaTextsEnabled ? [{ prefix: './assets/i18n/beta/' }] : [])
        ];
      }
    })
  ]
})
export class AppModule {}

Dynamic sources

Texts fetched from a translation service at runtime. They are off by default and, once configured, load only in DEV unless you list more stages. See Environments.

4. Dynamic loading from Weblate

project and component are the slugs shown in the Weblate UI — they say nothing about your app. The request goes to https://weblate.nivel.bazg.admin.ch/api/translations/my-project/my-component/{lang}/file/.

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: [{ project: 'my-project', component: 'my-component' }]
    })
  ]
})
export class AppModule {}

List several sources to load more than one component. Each one is a separate request, and they are merged in the order you write them.

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: [
        { project: 'my-project', component: 'checkout' },
        { project: 'my-project', component: 'orders' }
      ]
    })
  ]
})
export class AppModule {}

5. Dynamic loading from another Weblate instance

Only the instance changes, never the path:

https://weblate.nivel.bazg.admin.ch  /api/translations/my-project/checkout/de/file/
└──────── instance ───────────────┘  └──── project, component, language ─────────┘

Set it on a single source:

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: [{ project: 'my-project', component: 'my-component', url: 'https://weblate.example.ch' }]
    })
  ]
})
export class AppModule {}

Or once for all of them, so you do not repeat the instance on every source:

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: [
        { project: 'my-project', component: 'checkout' },
        { project: 'my-project', component: 'orders' }
      ]
    })
  ],
  providers: [{ provide: QD_TRANSLATION_PROVIDER_URL, useValue: 'https://weblate.example.ch' }]
})
export class AppModule {}

A source with its own url still wins over the token.

6. Dynamic loading switched by backend config

Return an empty array to use no dynamic source at all.

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: () => {
        const config = inject(MyAppConfigService);

        return config.externalTranslationsEnabled ? [{ project: 'my-project', component: 'my-component' }] : [];
      }
    })
  ]
})
export class AppModule {}

7. Dynamic loading from any other endpoint

A function as url builds the whole request URL itself, so the source does not have to be Weblate. The response must be a flat JSON object of translation keys.

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: [{ url: (lang: string) => `${appEnvironment.BACKEND_SERVICE_API}ui-api/i18n/${lang}` }]
    })
  ]
})
export class AppModule {}

8. Standalone app

Same options, no NgModule. Remember to import TranslateModule where you use the pipe.

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideAppInitializer(() => inject(MyAppConfigService).load()),
    provideQdLanguage({ dynamic: [{ project: 'my-project', component: 'my-component' }] })
  ]
};

Environments

A configured dynamic source loads in DEV only. Any other stage has to be listed explicitly — that decision belongs to the team that owns the app.

@NgModule({
  imports: [
    QdLanguageModule.forRoot({
      dynamic: [{ project: 'my-project', component: 'my-component', environments: ['DEV', 'REF'] }]
    })
  ]
})
export class AppModule {}

The library compares against QD_CURRENT_ENVIRONMENT, ignoring case. Fill that token from pamsEnvironment, which your backend returns from the auth config endpoint.

@NgModule({
  providers: [{ provide: QD_CURRENT_ENVIRONMENT, useFactory: () => inject(MyAppConfigService).pamsEnvironment }]
})
export class AppModule {}

Without the token no dynamic source is loaded and the library warns once. This keeps an app that is wired up incorrectly from calling a translation provider by accident.

How sources are merged

All sources are merged into one object. Later sources win over earlier ones, and static sources always come before dynamic ones.

// './assets/i18n/framework/de.json'  → { "i18n.qd.dialog.save": "Speichern" }
// './assets/i18n/de.json'            → { "demo.title": "Lokal" }
// Weblate                            → { "demo.title": "Aus Weblate" }
// Result                             → { "i18n.qd.dialog.save": "Speichern", "demo.title": "Aus Weblate" }

When a source fails

Every source catches its own errors, returns nothing and logs one message. The other sources still load, so a broken endpoint never leaves the app without texts.

A dynamic source that does not answer within 5 seconds is dropped as well. Without that limit it would hold back every other source, because all of them are awaited together.

Quadrel Framework | QdLanguage - Translation file not found: ./assets/i18n/de.json
Quadrel Framework | QdLanguage - Dynamic translations not loaded: https://weblate…/file/
Quadrel Framework | QdLanguage - Dynamic translations timed out after 5000 ms: https://weblate…/file/
Quadrel Framework | QdLanguage - Source did not return a translation object: ./assets/i18n/de.json

The last message means the response was not a flat JSON object — an array, null, or an HTML error page returned with status 200. Such a source is ignored instead of being merged.

Keep at least one static source. An app whose only source is dynamic shows raw keys as soon as the network fails.

Notes

  • @ngx-translate/core is a peer dependency. Your app has to install it.