Skip to content

chore: precompile translations to AST#1929

Merged
ErikSin merged 16 commits into
developfrom
chore-precompile-to-ast
Jun 24, 2026
Merged

chore: precompile translations to AST#1929
ErikSin merged 16 commits into
developfrom
chore-precompile-to-ast

Conversation

@ErikSin

@ErikSin ErikSin commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

closes #1879
closes #539

Also Lazy load translations so they are not all initially loaded in memory. Uses useQuery to deal with the async nature of dynamically loading the translations.

I also had to update the tsconfig to "module": "esnext" as it was throwing an error with the dynamic imports

@ErikSin ErikSin changed the title chore: update build script chore: precompile translations to AST Jun 10, 2026
@ErikSin ErikSin requested a review from achou11 June 10, 2026 22:22

@achou11 achou11 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionality seems to work as intended, but think there's room to clean up the implementation.

I verified that dynamic imports are working by checking using expo atlas (see https://docs.expo.dev/guides/analyzing-bundles/).

I was trying to test changes to the system settings using an emulator but seems like useLocales() doesn't update when that occurs. Might be a dev-specific issue, as it doesn't work on dev branch either (but works with built APK). Would be good to build an apk and test that changing the language via phone settings has expected results in the app.

Comment thread jest.setup.js

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind explaining why the changes here are necessary? It's not immediately clear to me... (aside from tests failing without the change)

@ErikSin ErikSin Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest doesn't support esm dynamic importing out of the box. So im just resolving the promise before hand in the jest environment (so it is no longer a dynamic import). We also dont need any other languages besides english in our jest tests, so i was only resolving english. But on reflection i'm just going to resolve all the languages in case we need them for future tests

Comment thread src/frontend/contexts/IntlContext.tsx Outdated
import {StyleSheet, Text} from 'react-native';
import {useLocales} from 'expo-localization';
import {useQuery, keepPreviousData} from '@tanstack/react-query';
import type {MessageFormatElement} from '@formatjs/icu-messageformat-parser';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably preferable to use the export from react-intl instead:

Suggested change
import type {MessageFormatElement} from '@formatjs/icu-messageformat-parser';
import type {MessageFormatElement} from 'react-intl';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest updating this script to use the official formatjs tooling for compilation.

Implemented an example alternative script that maintains existing behavior but uses @formatjs/cli-lib: 86219bc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/build-translations.mjs Outdated
import path from 'node:path';
import fs from 'node:fs';
import {readFile, writeFile} from 'node:fs/promises';
import {parse} from '@formatjs/icu-messageformat-parser';

@achou11 achou11 Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment about updating this file as a whole. seeing usage of this module - which is pretty low-level and probably not meant for direct usage - raised some alarms for me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/frontend/contexts/IntlContext.tsx Outdated
Comment on lines +42 to +64
const {data: messagesToUse, isPending} = useQuery({
queryKey: ['messages', ...languages],
queryFn: async () => {
const results = await Promise.all(
// reversing languages mean the highest priority languages get merged last and overwites the lower priority languages
languages
.reverse()
.map(
tag =>
localeImports[tag]?.().then(m => m.default) ??
Promise.resolve({}),
),
);
const merged: Record<string, MessageFormatElement[]> = {};
for (const msgs of results) {
Object.assign(merged, msgs);
}
return merged;
},
staleTime: Infinity,
// see: https://tanstack.com/query/latest/docs/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata
placeholderData: keepPreviousData,
});

@achou11 achou11 Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this works, I think it'd be more idiomatic to use useQueries here. The main reasoning is to isolate each language with their own query to avoid unnecessary work when they change.

Implemented an example here: d7b1135 (note that it might need some adjustments...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/build-translations.mjs Outdated
Comment thread scripts/build-intl-polyfills.mjs Outdated
import languages from '../src/frontend/languages.json' with {type: 'json'};
import messages from '../translations/messages.json' with {type: 'json'};

const TRANSLATIONS_DIR = new URL('../translations/', import.meta.url).pathname;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to update this in order to be used on a non-posix OS.

Suggested change
const TRANSLATIONS_DIR = new URL('../translations/', import.meta.url).pathname;
const TRANSLATIONS_DIR = fileURLToPath(new URL('../translations/', import.meta.url));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@awana-lockfile-bot

Copy link
Copy Markdown

package-lock.json changes

Click to toggle table visibility
Name Status Previous Current
@formatjs/cli-lib ADDED - 8.7.10
@formatjs/cli-native-darwin-arm64 ADDED - 1.1.5
@formatjs/cli-native-linux-arm64-musl ADDED - 1.0.3
@formatjs/cli-native-linux-arm64 ADDED - 1.2.5
@formatjs/cli-native-linux-x64-musl ADDED - 1.0.3
@formatjs/cli-native-linux-x64 ADDED - 1.1.5
@formatjs/cli-native-win32-x64 ADDED - 1.1.6
@formatjs/ts-transformer ADDED - 4.4.13
@types/fs-extra ADDED - 11.0.4
@types/jsonfile ADDED - 6.1.4
array-find-index ADDED - 1.0.2
currently-unhandled ADDED - 0.4.1
loud-rejection ADDED - 2.2.0

@socket-security

socket-security Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​formatjs/​cli-lib@​8.7.10981007298100

View full report

@ErikSin ErikSin requested a review from achou11 June 23, 2026 21:41

@achou11 achou11 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of non-blocking suggestions but otherwise changes look good

Comment thread scripts/build-translations.mjs Outdated
Comment on lines +36 to +40
if (
!Object.prototype.hasOwnProperty.call(
LANGUAGE_NAME_TRANSLATIONS,
languageCode,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very subjective: this feels more readable at a glance and achieves the same thing:

Suggested change
if (
!Object.prototype.hasOwnProperty.call(
LANGUAGE_NAME_TRANSLATIONS,
languageCode,
)
if (!(languageCode in LANGUAGE_NAME_TRANSLATIONS)) {

*
* @description The tanstack query option `placeholderData` is set to `keepPreviousData`. This means this query will only ever be `pending` on initial load
*/
export function useLanguageQueries(languageCodes: AvailableLanguageTag[]) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defining a separate hook that just wraps a query hook seems unnecessary right now. especially since this only used in one place, I would recommend inlining it where it's used.

If you have a need for reusing it elsewhere, usually the recommended approach is to define a query options object and passing that into the relevant query hook in the calling component (see https://tkdodo.eu/blog/the-query-options-api).

@ErikSin ErikSin merged commit fbb0ad9 into develop Jun 24, 2026
9 checks passed
@ErikSin ErikSin deleted the chore-precompile-to-ast branch June 24, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Precompile translations into AST Potentially address warning from @formatjs/intl about not pre-compiling messages

2 participants