Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Single PWA with i18n #43796

Open
TheSlimvReal opened this issue Oct 11, 2021 · 12 comments
Open

Single PWA with i18n #43796

TheSlimvReal opened this issue Oct 11, 2021 · 12 comments
Labels
area: i18n area: service-worker Issues related to the @angular/service-worker package feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Milestone

Comments

@TheSlimvReal
Copy link

Which @angular/* package(s) are relevant/releated to the feature request?

localize, service-worker

Description

Following up on this post on stackoverflow.

After adding internationalization (i18n) to our app we run into the big problem of backwards compatibility. Before adding i18n we had our service worker at the root folder and after adding i18n we now have a separate service worker for each language we translated the app into. This is a problem because a user that installed the app prior to this change will not correctly get updates because the old service worker will not be able to find the new service worker.

This is the default behavior of Angular and left us with the decision that we either have to tell all our users that they will have to uninstall and re-install the app or add a service worker in the root folder that automatically takes care of the migration and leave it deployed until all users are migrated. Theses solutions are rather unsatisfying, poorly documented and mean that a we would have to go through the same struggle again if we decide to remove the i18n or take a different approach for it.

Proposed solution

It would be great if Angular would not enforce to use the one-PWA-per-language approach but rather leave this as a choice to the developer. This would make the transition from a single- to a multi-language app much easier and leaves the option open to also go back to the previous approach (no i18n).

This could be configured through the compiler options in the angular.json file. E.g. rootServiceWorker: true.

Alternatives considered

Alternatively, it would be great if Angular provided a guide or a predefined service worker (similar to the safety-worker.js) that allows a easy migration of the PWAs when i18n is introduced.

@ngbot ngbot bot added this to the needsTriage milestone Oct 11, 2021
@petebacondarwin petebacondarwin added the area: service-worker Issues related to the @angular/service-worker package label Oct 12, 2021
@TheSlimvReal
Copy link
Author

TheSlimvReal commented Oct 21, 2021

We are currently using the following script to merge the service workers and unregister the old ones:

const fs = require("fs");

const distFolder = "dist";

function getNgswConfig(locale) {
  const swConfig = fs.readFileSync(`${distFolder}/${locale}/ngsw.json`);
  return JSON.parse(swConfig);
}

const locales = fs
  .readdirSync(distFolder)
  .filter((locale) => fs.lstatSync(`${distFolder}/${locale}`).isDirectory());

// Merge the ngsw.json files
const firstLocale = locales.pop();
const combined = getNgswConfig(firstLocale);
locales.forEach((locale) => {
  const additional = getNgswConfig(locale);

  // Merge data and asset groups
  const toBeMergedGroups = [
    { groupType: "dataGroups", property: "patterns" },
    { groupType: "assetGroups", property: "urls" },
  ];
  toBeMergedGroups.forEach(({ groupType, property }) => {
    additional[groupType].forEach((group) => {
      combined[groupType]
        .find((g) => g.name === group.name)
        [property].push(...group[property]);
    });
  });

  // combine hash tables
  Object.assign(combined.hashTable, additional.hashTable);
  fs.unlinkSync(`${distFolder}/${locale}/ngsw.json`);
  fs.unlinkSync(`${distFolder}/${locale}/ngsw-worker.js`);
  fs.renameSync(
    `${distFolder}/${locale}/safety-worker.js`,
    `${distFolder}/${locale}/ngsw-worker.js`
  );
});

combined.index = "/index.html";
// add root index.html to correctly cache direct app entry (e.g. without en-US)
combined["assetGroups"][0]["urls"].push("/index.html");

fs.writeFileSync(`${distFolder}/ngsw.json`, JSON.stringify(combined));
fs.unlinkSync(`${distFolder}/${firstLocale}/ngsw.json`);

// Adjust service worker to allow changing language offline
const swFile = fs
  .readFileSync(`${distFolder}/${firstLocale}/ngsw-worker.js`)
  .toString();
const patchedSw = swFile.replace(
  "return this.handleFetch(this.adapter.newRequest(this.indexUrl), context);",
  `const locale = this.adapter.normalizeUrl(req.url).split("/")[1];
                    const url = locale ? "/" + locale + "/index.html": "/index.html";
                    return this.handleFetch(this.adapter.newRequest(url), context);`
);
fs.writeFileSync(`${distFolder}/ngsw-worker.js`, patchedSw);
fs.unlinkSync(`${distFolder}/${firstLocale}/ngsw-worker.js`);
fs.renameSync(
  `${distFolder}/${firstLocale}/safety-worker.js`,
  `${distFolder}/${firstLocale}/ngsw-worker.js`
);

This script:

  1. merges all the ngsw.json files into a single one that is placed at root
  2. patches the ngsw-worker.js file to allow changing languages offline and places it at root
  3. removes all the ngsw.json files in the locale folders
  4. Replaces the ngsw-worker.js files with the safety-worker.js files in the locale folders to unregister all old service workers that might still be present on the users browsers

Additionally we register the service worker like this:

    ServiceWorkerModule.register("/ngsw-worker.js", {
      enabled: environment.production,
    }),

And our manifest file has this config:

{
  "scope": "/",
  "start_url": "/",
  ...
}

More information can be found in this repo where we use this approach in production.

@petebacondarwin petebacondarwin added the feature Issue that requests a new feature label Oct 21, 2021
@ngbot ngbot bot modified the milestones: needsTriage, Backlog Oct 21, 2021
@petebacondarwin
Copy link
Member

Hi @TheSlimvReal - sorry not to respond so far on this issue. It is a bit tricky to implement on our side. I need to talk with @gkalpak about how this could work - I am not sure if it is something we would do. I am glad that for the time being you appear to have a workaround.

@angular-robot
Copy link
Contributor

angular-robot bot commented Oct 21, 2021

This feature request is now candidate for our backlog! In the next phase, the community has 60 days to upvote. If the request receives more than 20 upvotes, we'll move it to our consideration list.

You can find more details about the feature request process in our documentation.

@angular-robot angular-robot bot added the feature: votes required Feature request which is currently still in the voting phase label Oct 21, 2021
@petebacondarwin
Copy link
Member

Can you clarify what the problem is for new users?

Is it that when they arrive at https://my.domain.com/ they hit the old SW, but really you want them to be redirected to some locale specific URL like https://my.domain.com/en-US?

@TheSlimvReal
Copy link
Author

No, the problem initially came from us adding i18n to our app but old users, who installed the app before i18n was added, never got updates after that because the location of the service worker file changed.

One solution is to tell all users to reinstall the app which is a rather difficult process in our case and it might introduce the same problem again if we decide to remove i18n or take a different approach. Therefore, it would be great if there would be an option to keep the service worker at the root folder (like what the script does that I have provided). Also this could allow users to change the language offline without having to install all languages as separate PWAs but that's probably a different question.

@angular-robot
Copy link
Contributor

angular-robot bot commented Nov 30, 2021

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

@angular-robot angular-robot bot added feature: under consideration Feature request for which voting has completed and the request is now under consideration and removed feature: votes required Feature request which is currently still in the voting phase labels Dec 1, 2021
@ValentinFunk
Copy link

If you load your translation files at runtime using @angular/localize you can have a single pwa with multiple languages. You can still lazy load the translation files so that all languages are not part of your main bundle

@TheSlimvReal
Copy link
Author

TheSlimvReal commented Jan 18, 2022

If you load your translation files at runtime using @angular/localize you can have a single pwa with multiple languages. You can still lazy load the translation files so that all languages are not part of your main bundle

@kamshak What exactly are you revering to when you talk about at runtime? @angular/localize uses the translation files at build time and creates a single application for each language. The translation files are not part of the final main bundle.

@ValentinFunk
Copy link

If you load your translation files at runtime using @angular/localize you can have a single pwa with multiple languages. You can still lazy load the translation files so that all languages are not part of your main bundle

@kamshak What exactly are you revering to when you talk about at runtime? @angular/localize uses the translation files at build time and creates a single application for each language. The translation files are not part of the final main bundle.

Yes, you can build separate application bundles for each language (inlining the translations). You use @angular/localize only for tagging strings. As a postbuild step the CLI will basically get rid of all the $localize calls in your code by replacing the calls to it with the translations more or less directly. The issue is you need to now serve separate PWAs for each language because you essentially have different apps with different file hashes and everything.

Another option is to ship translation files with your app and load them at run time (i.e. generate one app bundle + multiple files containing only the messages). That way Angular will keep the actual calls to $localize in your app, which then perform the translations when the app is running. To do this you include @angular/localize/init and call loadTranslations at runtime. Now what I'm referring to is this: you need to have messages available so you want to ship the translation file with your app. But you don't want to include ALL languages into your main bundle. So you can write a small bit of bootstrap logic to get the user's language, then load only the needed translation file on demand before the app bootstraps. I can do a small writeup on how to wire this up exactly if there's interest

@TheSlimvReal
Copy link
Author

@kamshak Thank you for pointing that out, I had no idea that this loadTranslations function exists. I guess I have to try out whether the performance is still alright. At least it keeps the PWA size smaller. Have you ever worked with that approach?

@ValentinFunk
Copy link

@kamshak Thank you for pointing that out, I had no idea that this loadTranslations function exists. I guess I have to try out whether the performance is still alright. At least it keeps the PWA size smaller. Have you ever worked with that approach?

Have a PWA in production with this, it's working great so far

@TheSlimvReal
Copy link
Author

TheSlimvReal commented Apr 22, 2022

@kamshak Sorry for the long silence. My previously mentioned workaround was working quite well but now we are running into some new problems regarding the service-workers and I would like to try out your approach.

Just out of curiosity: How do you create your translation files for the loadTranslations function? Do you use the XLIF format and then parse it using something like the merge script described here?
It would be great if you could point me to a reference implementation where loadTranslations is used as the docs are very sparse on that point (especially regarding caching and bootstrap).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: i18n area: service-worker Issues related to the @angular/service-worker package feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests

4 participants