Skip to content

Commit

Permalink
Use Spanish translations in the UI
Browse files Browse the repository at this point in the history
## Description

Finally we're here: you can just set `language="es"` on the calculator
element and it'll be in Spanish.

Getting the strings out of the XLIFF file and into code requires
running `lit-localize build`. Rather than making people do that
manually, I wrote a Parcel resolver that runs the command behind the
scenes. Overengineered? Maybe! But I learned a bunch about Parcel!

Note about switching `language` dynamically. Apart from the problem I
noted in the comment on the `language` attribute (text that came back
from the API will not change language until the next API fetch),
there's another problem, with the Shoelace select elements: the text
shown in them won't change until you make a new selection in the
element. I _think_ it may be related to
[this](shoelace-style/shoelace#1570); in any
case, once all the immediate i18n work is wrapped up I may try to
isolate the issue and file an issue with them if it's not the same
bug. With this bug, dynamically setting the attribute _on page load_
will leave a couple of untranslated strings, and I think that's a use
case we do want to support.

## Test Plan

Add a `language="es"` attribute to the main element in
`rhode-island.html`, and make sure the UI shows up in Spanish. Query
for incentives; make sure the program names of federal incentives show
up in Spanish. (Those are the only thing localized on the backend
right now.)
  • Loading branch information
oyamauchi committed Nov 14, 2023
1 parent 581f0a6 commit cb3f239
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ yarn-error.log*
# cypress
cypress/videos/*.mp4
cypress/screenshots/*

# lit-localize generated
generated
3 changes: 2 additions & 1 deletion .parcelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extends": ["@parcel/config-default"],
"reporters": ["...", "parcel-reporter-static-files-copy"]
"resolvers": ["./scripts/parcel-resolver-locales.mjs", "..."],
"reporters": ["...", "parcel-reporter-static-files-copy"]
}
44 changes: 44 additions & 0 deletions scripts/parcel-resolver-locales.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Resolver } from '@parcel/plugin';
import { exec } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { promisify } from 'util';

async function allXlfFiles(projectRoot) {
const entries = await promisify(fs.readdir)(
path.join(projectRoot, 'translations'),
);
return entries.map(entry => path.join(projectRoot, 'translations', entry));
}

/**
* Resolves import specifiers starting with `locales:` by running lit-localize
* to generate strings files, and pointing Parcel at the generated files.
*
* NB: this is not a Typescript file! (Parcel doesn't support plugins written
* in TS.) No type checking!
*/
export default new Resolver({
async resolve({ specifier, options: { projectRoot } }) {
if (specifier.startsWith('locales:')) {
await promisify(exec)('npx lit-localize build');

const locale = specifier.substring('locales:'.length);
const filePath =
locale === 'config'
? path.join(projectRoot, 'generated/locales.ts')
: path.join(projectRoot, `generated/strings/${locale}.ts`);

return {
// Rebuild if an XLIFF file changes, or the lit-localize config.
invalidateOnFileChange: [
...(await allXlfFiles(projectRoot)),
path.join(projectRoot, 'lit-localize.json'),
],
filePath,
};
} else {
return null;
}
},
});
14 changes: 14 additions & 0 deletions src/parcel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,17 @@ declare module 'bundle-text:*' {
const value: string;
export default value;
}

/**
* We use the magic "locales" scheme to import files that are generated by
* lit-localize. Running lit-localize and resolving these magic specifiers
* happen in scripts/parcel-resolver-locale.mjs.
*/
declare module 'locales:config' {
export const sourceLocale: string;
export const targetLocales: string[];
}
declare module 'locales:*' {
import { TemplateMap } from '@lit/localize';
export const templates: TemplateMap;
}
35 changes: 34 additions & 1 deletion src/state-calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,20 @@ import { submitEmailSignup, wasEmailSubmitted } from './email-signup';
import SlSelect from '@shoelace-style/shoelace/dist/components/select/select';
import { safeLocalStorage } from './safe-local-storage';
import scrollIntoView from 'scroll-into-view-if-needed';
import { localized, msg, str } from '@lit/localize';
import { configureLocalization, localized, msg, str } from '@lit/localize';
import { sourceLocale, targetLocales } from 'locales:config';

// See scripts/parcel-resolver-locale.mjs for how this import is resolved.
const { setLocale } = configureLocalization({
sourceLocale,
targetLocales,
loadLocale: locale =>
locale === 'es'
? import('locales:es')
: (() => {
throw new Error(`unknown locale ${locale}`);
})(),
});

const loadingTemplate = () => html`
<div class="card card-content">
Expand Down Expand Up @@ -147,6 +160,15 @@ export class RewiringAmericaStateCalculator extends LitElement {
formTitleStyles,
];

/**
* Property to control display language. Changing this dynamically is not
* supported: UI labels and such will change immediately, but user-visible
* text that came from API responses will not change until the next API
* fetch completes.
*/
@property({ type: String, attribute: 'language' })
language: string = 'en';

/* supported properties to control showing/hiding of each card in the widget */

@property({ type: Boolean, attribute: 'hide-form' })
Expand Down Expand Up @@ -308,6 +330,15 @@ export class RewiringAmericaStateCalculator extends LitElement {
this.initFormProperties();
}

/**
* Make sure the locale is set before rendering begins. setLocale() is async
* and this is the only async part of the component lifecycle we can hook.
*/
protected override async scheduleUpdate(): Promise<void> {
await setLocale(this.language);
super.scheduleUpdate();
}

override async updated() {
await new Promise(r => setTimeout(r, 0));
if (!this.renderRoot) {
Expand Down Expand Up @@ -357,6 +388,7 @@ export class RewiringAmericaStateCalculator extends LitElement {
autoRun: false,
task: async () => {
const query = new URLSearchParams({
language: this.language,
'location[zip]': this.zip,
});

Expand Down Expand Up @@ -403,6 +435,7 @@ export class RewiringAmericaStateCalculator extends LitElement {
}

const query = new URLSearchParams({
language: this.language,
'location[zip]': this.zip,
owner_status: this.ownerStatus,
household_income: this.householdIncome,
Expand Down

0 comments on commit cb3f239

Please sign in to comment.