Multilingual Angular App hosted on Firebase and Surge with the same build
Tags: angular, webdev, firebase, tutorial
Published: 2022-08-22 04:57:42

Last time we figured out a way to prefix all routes without changing the `base href`, using `APP_BASE_HREF` token in Angular, which helped us host on Netlify a same-build multilingual App. Today we shall try Firebase and [Surge.sh](http://Surge.sh).

Hosting on Firebase
-------------------

I started writing the host configuration for Firebase thinking it had more to offer, but I was disappointed. The hosting configuration does not allow **conditions** which Netlify allows. We will resolve to a serverless function at one point, which is not provided on the Free plan. Let's dig in.

### Setup Firebase locally

To be able to **test locally**, we need to have at least two files sitting in a host folder, and a **global cli**.

`npm install -g firebase-tools`

In [StackBlitz](https://stackblitz.com/edit/angular-runtime-translation) find the Firebase dedicated host folder under `/host-firebase`

The two files are `.firebaserc` and `firebase.json`

```json
// .firebaserc in the root of the firebase host
{
  "projects": {
    // does not have to exist in the cloud, we are running locally
    "default": "cr"
  }
}

```

The `firebase.json` we will build up with hosting rules, initially it looks like this:

```json
// firebase.json in the root of the firebase host
{
  "hosting": [
    {
      "target": "web",
			// the public folder is where the build will go
      "public": "client",
      "ignore": [
        "firebase.json",
        "**/.*",
        "**/node_modules/**"
      ],
      "rewrites": [
				// TODO
      ]
    }
  ]
}

```

To host on Firebase a browser-only app, all files must sit in the same folder, like in Netlify hosting, including any external configuration. On [StackBlitz](https://stackblitz.com/edit/angular-runtime-translation) find the example build under `/host-firebase`. Our `writeindex` task also copies into the same `host/client` folder as in Netlify setup (find example in `/src/firebase/angular.json`.)

To run locally, change directory into the `host` folder and run:

`firebase emulators:start`

Browse to `http://localhost:5000` and I hope you see what I see. Let's begin with the URL based app:

### URL driven apps

We face the same issue of **differentiating static resources from friendly URLs** as in Netlify hosting, but the solution of conditional rules is not available in Firebase, so we use `APP_BASE_HREF` right away.

Since all assets are served locally, we just need to rewrite the friendly URLs

```json
// firebase.json for URL based app with APP_BASE_URL set in language script
"rewrites": [
  {
    "source": "/ar{,/**}",
    "destination": "/index.ar.html"
  },
	// default others
  {
    "source": "**",
    "destination": "/index.en.html"
  }
]

```

The missing opportunity is `/` root URL for a second time user. There are some solutions to that, some of them are **hacky**: making use of `index.html`, and others need a little more work: relying on [Firebase i18n rewrite](https://firebase.google.com/docs/hosting/i18n-rewrites).

#### Make use of the root `index.html`

Create an empty HTML page with the following script

```html
<!DOCTYPE html>
<html lang="en">
<head>
 <script>
  const cookie = document.cookie;
  let root = 'en';
  // use any cookie name, redirect according to value
  if (cookie.indexOf('cr-lang') > -1) {
    // this line was produced by CoPilot! Not bad!
    root = cookie.split('cr-lang=')[1].split(';')[0];
  }
  // replace URL, a client 301 redirect
  window.location.replace(`/${root}`);
 </script>
</head>
<body>

</body>
</html>

```

Don't forget to add this index file to the `angular.json` assets to copy over to `client` folder.

#### Use Firebase i18n rewrites

For this to work, the files must be physically saved under `en` and `ar` folders. So we need to **change our Angular builder**, or gulp task a bit, to support this folder structure:

```
|-client/
|----en/
|------index.html
|----ar/
|------index.html
|--assets...

```

Our `firebase.json` then looks like this

```json
// to handle already selected language in cookies
// cookie name must be firebase-language-override
"i18n": {
  "root": "/"
},
"rewrites": [
  {
    "source": "/ar{,/**}",
    "destination": "/ar/index.html"
  },
  {
    "source": "**",
    "destination": "/en/index.html"
  }
]

```

We can adjust the builder to create this new structure easily, find the new code in StackBlitz under its own builder `builder/locales/folders.ts`:

```ts
// update builder to build folder structure
interface Options  {
	// ...
	// new property
  fileName?: string;
}

export default createBuilder(LocalizeIndex);

function LocalizeIndex(
    options: Options,
    context: BuilderContext,
): BuilderOutput {
    // ...
		// instead of writing file, first create folder
    if (!existsSync(options.destination + '/' + lang.name)){
          mkdirSync(options.destination + '/' + lang.name);
    }
    // save file with index.html, base href = /
     writeFileSync(`${options.destination}/${lang.name}/${ options.fileName || 'index.html'}`, contents);

		// ...
    return { success: true };
}

```

In our config (or external configuration), set the cookie name to `firebase-language-override`

```ts
// src/app/config.ts
export const Config = {
  Res: {
    cookieName: 'firebase-language-override',
		//...
  },
};

```

Building, running locally, switching, works. Let's move on to the cookie based solution.

### Cookie driven apps

We back up a bit to the normal app, with no APP_BASE_HREF prefix. There are no conditional rewrite rules in Firebase, there is however an [i18n solution](https://firebase.google.com/docs/hosting/i18n-rewrites), and there are serverless functions. Let's try both:

#### Firebase i18n rewrites:

The following is the sequence of events in a hosting file:

-   browse to `/`, the host detects Language from browser and serves `/en/index.html` which is a physical file
-   browsing to `/products` however, in an Angular app, the host will try to serve `/en/products/index.html` which does not exist, so it tries to serve `/en/404.html`
-   this `/en/404.html` can have the same contents of the `/en/index.html` file and load the Angular app itself, but it is a bad practice since it registers as a `404`
-   We can also create a JavaScript redirect in `404.html` to `/` but this too is not a good solution since we would lose the friendly URL

The folder structure would look like this

```
|-client/
|----en/
|------index.html
|------404.html
|----ar/
|------index.html
|------404.html
|--assets...

```

That's a reason too many to bog me down, to top it all, the localized `404.html` does not [run locally in the emulator](https://github.com/firebase/firebase-tools-ui/issues/844), so I could not test it. Moving on.

#### Firebase serverless function

The Spark "free" plan does not run functions in Firebase, the first paid plan: Blaze however; is quite generous in its free tier. You can try this locally, but to deploy it, you need a Blaze subscription.

To run functions locally, without having a real application, you need to do the following:

-   Create `functions` folder inside the firebase `host` folder (find it in StackBlitz under `host-firebase`)

-   Create a `functions/package.json`

    ```json
    // package.json inside functions need nothing else
    {
    	// that's the only dependency you need to install
      "dependencies": {
        "firebase-functions": "^3.22.0"
      },
      // add this, or in firebase.json add runtime, see below
      "engines": {
        "node": "16"
      }
    }

    ```

-   Add `engines` node to `package.json` as above, following [documentation](https://firebase.google.com/docs/functions/get-started#initialize-your-project) of firebase:

> The `package.json` file created during initialization contains an important key: `"engines": {"node": "10"}`. This specifies your Node.js version for writing and deploying functions. You can [select other supported versions](https://firebase.google.com/docs/functions/manage-functions#set_nodejs_version).

Or following an error message in my command line, you can create it in `firebase.json`:

```json
// firebase.json in root of host, undocumented
{
	"hosting": ...
	"functions": {
		"runtime": "nodejs16"
	}
}

```

-   Install the only dependency you need inside the functions folder `firebase-functions`
-   In `host`, run `firebase emulators:start`
-   Ignore funny warning messages and browse normally

The function we want to create is a catch-all to send the right `index.[lang].html` according to a cookie value:

```js
// functions/index.js
const functions = require('firebase-functions');

exports.serveLocalized = functions.https.onRequest((req, res) => {
  // find cr-lang cookie
	// Pssst: you can require a config.json
  // if you use external configuration to read the cookie name
  const cookie = req.headers.cookie;
  let lang = 'en';
  if (cookie.indexOf('cr-lang') > -1) {
    // CoPilot!
    lang = cookie.split('cr-lang=')[1].split(';')[0];
  }
  // serve the right index file
  res.status(200).sendFile(`index.${lang}.html`, {root: '../client/'});
});

```

Under `hosting` in `firebase.json`

```json
// ...
"rewrites": [
  {
    "source": "**",
    "function": "serveLocalized"
  }
]

```

Now run emulator, and browse to `http://locahost:5000`

The rule in Firebase hosting is: if the file physically exists it shall be served first. So assets are statically served.

> We previously created **[external configuration](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal)** in Angular, one of the benefits for this project is now we can have the same build hosted by different hosts, each has its own configuration. We can also access this configuration in Firebase functions with a simple `require` statement.

Testing, switching languages in cookies, works. Moving on.

Hosting on Surge
----------------

When I decided to try and host it on the Free subscription of Surge, I had little hope, because the ROUTE file is not supported. But I was pleasantly surprised. [Surge.sh](http://Surge.sh) has a publishing tool. To use, make sure you have installed surge globally:

`npm install surge -g`

Also, the `CNAME` file should be in the published folder, we shall include the file in our `assets` array in `angular.json`.

To publish, we move into our `host` folder, and run `surge ./client` (assuming we build into `client` sub folder).

Find the example host under StackBlitz `/host-surge` folder.

### The only available option

We do not have many options:

-   Cookie based apps need conditional rewrite, which is not supported
-   URL based with `base href` set to a specific language, need to rewrite assets, which also is not an option
-   With `APP_BASE_URL`, the served file is `index.html`, and if not found, `200.html` on root, but in either case, we would not know which `index.[lang].html` to serve

That leaves us with one option:

-   Use `APP_BASE_HREF`
-   Create physical language folders, for each language.

```
|-client/
|----en/
|------200.html
|----ar/
|------200.html
|--assets...

```

-   With our Angular builder (or Gulp task) `writeindex`, generate `200.html` in each language folder, which will handle rewriting `/en/products/...` to `/en/200.html`.
-   Instead of making `index.html` the default Angular app (which works fine), we can make it redirect via JavaScript to the correct sub folder according to a cookie.

```html
<!DOCTYPE html>
<html lang="en">
<head>
 <script>
  const cookie = document.cookie;
  let root = 'en';
  // use any cookie name, redirect according to value
  if (cookie.indexOf('cr-lang') > -1) {
    root = cookie.split('cr-lang=')[1].split(';')[0];
  }
  // replace URL, a client 301 redirect
  window.location.replace(`/${root}`);
 </script>
</head>
<body>
</body>
</html>

```

Surging... running, redirecting, switching language, celebrating. 💃🏼💃🏼

I'm actually impressed and pleased with `Surge`

Conclusion
----------

The last bit of the puzzle to twist Angular localization, is extracting the translatable keys into the right JavaScript file. To do that, and to conclude our series, come back next week. 😴

### RESOURCES

-   [StackBlitz project](https://stackblitz.com/edit/angular-runtime-translation)
-   [Firebase Hosting Configuration](https://firebase.google.com/docs/hosting/full-config)
-   [Firebase i18n rewrites](https://firebase.google.com/docs/hosting/i18n-rewrites)
-   [Test Firebase locally](https://firebase.google.com/docs/hosting/test-preview-deploy)
-   [Surge Hosting](https://surge.sh/help/)
Thank you for reading yet another long article, did you spot the squiggles under the rock?

### RELATED POSTS
- [Loading external configurations in Angular Universal](https://garage.sekrab.com/posts/loading-external-configurations-in-angular-universal)

{% embed https://garage.sekrab.com/posts/multilingual-angular-app-hosted-on-firebase-and-surge-with-the-same-build %}