Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7158ae9
feat(i18n): register Greek locale
roryabraham Jul 9, 2026
4a7b373
feat(i18n): wire Greek runtime loaders
roryabraham Jul 9, 2026
dd01538
feat(i18n): add Greek translation prompt
roryabraham Jul 9, 2026
07e6750
feat(i18n): generate Greek translations
roryabraham Jul 9, 2026
27aa359
chore(lint): exclude generated el.ts from ESLint
roryabraham Jul 9, 2026
75992d1
chore(spell): exclude generated el files from cspell
roryabraham Jul 9, 2026
ec97470
chore(spell): add Greek native name to cspell words
roryabraham Jul 9, 2026
1b875f0
Merge remote-tracking branch 'origin/main' into rory-657039-greek-locale
roryabraham Jul 9, 2026
617a40a
fix(fonts): add system font fallback for uncovered scripts on web
roryabraham Jul 15, 2026
241fdc4
Merge remote-tracking branch 'origin/main' into rory-657039-greek-locale
roryabraham Jul 15, 2026
de41fa8
fix(i18n): fill in Greek translations missing after merging main
roryabraham Jul 15, 2026
7515b69
fix(fonts): keep HTML font stack fallbacks
roryabraham Jul 16, 2026
ff9a044
Merge remote-tracking branch 'origin/main' into rory-657039-greek-locale
roryabraham Jul 16, 2026
113cf0f
fix(i18n): sync Greek strings after main merge
roryabraham Jul 16, 2026
57c97ad
fix(i18n): wrap Greek LHN Inbox/Account labels cleanly
roryabraham Jul 16, 2026
c60d6b9
Merge remote-tracking branch 'origin/main' into rory-657039-greek-locale
roryabraham Jul 16, 2026
3eef94f
fix(i18n): add Greek strings for Rillet multi-account export
roryabraham Jul 16, 2026
a7708a8
Merge remote-tracking branch 'origin/main' into rory-657039-greek-locale
roryabraham Jul 17, 2026
1dfa914
fix(i18n): regenerate Greek strings after main merge
roryabraham Jul 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/eslint/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ const config = defineConfig([
'web/snippets/gib.js',
// Generated language files - excluded from ESLint but still type-checked
'src/languages/de.ts',
'src/languages/el.ts',
'src/languages/es.ts',
'src/languages/fr.ts',
'src/languages/it.ts',
Expand Down
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"Ephermeral",
"Erste",
"Español",
"Ελληνικά",
"Expatriot",
"Expensable",
"Expensi",
Expand Down Expand Up @@ -1082,6 +1083,7 @@
"ignorePaths": [
".gitignore",
"src/languages/de.ts",
"src/languages/el.ts",
"src/languages/es.ts",
"src/languages/fr.ts",
"src/languages/it.ts",
Expand All @@ -1091,6 +1093,7 @@
"src/languages/pt-BR.ts",
"src/languages/zh-hans.ts",
"prompts/translation/de.ts",
"prompts/translation/el.ts",
"prompts/translation/es.ts",
"prompts/translation/fr.ts",
"prompts/translation/it.ts",
Expand Down
31 changes: 31 additions & 0 deletions prompts/translation/el.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Str} from 'expensify-common';

import Glossary from './Glossary';

// U+200B ZERO WIDTH SPACE — allows a clean 2-line wrap in the narrow LHN tab rail
// without showing a hyphen. Preserve these code points when regenerating translations.
const ZWSP = '\u200B';

const greekGlossary = new Glossary([
// Branded product names
{sourceTerm: 'Expensify Card', targetTerm: 'Κάρτα Expensify', usage: 'Branded Expensify payment card'},

// LHN tab labels — single long words that otherwise wrap with a 1-character orphan
// or clip in the 72px navigation rail. Keep the embedded ZWSP at the break point.
{sourceTerm: 'Inbox', targetTerm: `Εισερχό${ZWSP}μενα`, usage: 'LHN tab label; keep the zero-width space so the word can wrap as Εισερχό/μενα'},
{sourceTerm: 'Account', targetTerm: `Λογαρια${ZWSP}σμός`, usage: 'LHN tab label; keep the zero-width space so the word can wrap as Λογαρια/σμός'},
]);

export default Str.dedent(`
When translating to Greek, follow these rules:

- Use polite/formal tone for user-facing text, consistent with standard Greek business software conventions.
- Keep UI labels concise and follow standard Greek capitalization rules (only sentence-initial words and proper nouns are capitalized).
- Pay close attention to Greek grammatical gender and case agreement (nominative, genitive, accusative) when branded terms or nouns appear in different sentence positions.
- Apply correct Greek plural forms and noun-adjective agreement around numeric placeholders.
- For glossary terms that contain a zero-width space (U+200B), preserve that exact character in the translation. It is intentional and enables clean line wrapping in narrow UI.

Use the following glossary for canonical Greek translations of common terms:

${greekGlossary.toXML()}
`);
2 changes: 2 additions & 0 deletions src/CONST/LOCALES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const FULLY_SUPPORTED_LOCALES = {
*/
const BETA_LOCALES = {
DE: 'de',
EL: 'el',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add native Intl locale data before enabling Greek

When this adds el to the supported locale list, Greek becomes selectable on iOS/Android too, but the native Intl polyfills still only load @formatjs/intl-pluralrules data for en/es in src/libs/IntlPolyfill/index.android.ts and index.ios.ts (and the iOS NumberFormat polyfill likewise only loads en/es). With Greek selected, rendering any pluralized translation in src/languages/el.ts (for example expenseCount) will construct Intl.PluralRules('el') after the forced polyfill has no Greek data loaded, causing missing-locale runtime failures on native; please load the Greek Intl polyfill data alongside making the locale selectable.

Useful? React with 👍 / 👎.

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.

@MelvinBot does this concern affect all locales other than English and Spanish, or just Greek? If it does concern other locales, please create a new GitHub issue in this repo, label it weekly, and assign to me

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This affects all locales other than English and Spanish — not just Greek. The native polyfills hardcode en/es data (index.android.ts:15-17, index.ios.ts:16-18, polyfillNumberFormat.ts:32-34), and this PR only wires up Greek for ListFormat — nothing Greek-specific about the pluralrules/NumberFormat gap. It's a pre-existing issue shared by every beta locale (de, el, fr, it, ja, nl, pl, pt-BR, zh-hans).

One correction to the concern above: @formatjs/intl-pluralrules polyfill-force does not crash on a missing locale — ResolveLocale falls back to the first-loaded default (en). So instead of a runtime failure, non-en/es locales silently use English plural rules:

  • Unaffected in practice: locales sharing English's one/other categories — de, el (Greek is one/other), fr, it, nl, pt-BR, and ja/zh-hans (other only). So Greek's expenseCount will actually render correctly via the en fallback.
  • Actually mis-served today: Polish (pl), which has one/few/many/other — English rules can only ever yield one/other, so its few/many forms never fire. Any future locale with non-English plural categories (Russian, Arabic, Czech…) would hit the same.
  • NumberFormat is only forced on devices with stale ICU, and again only loads en/es.

Filed as #96335, labeled Weekly and assigned to you. Suggested fix in there: load the pluralrules/numberformat data for the active locale dynamically, the same way IntlStore already loads translations, date-fns, and ListFormat data per locale.

IT: 'it',
JA: 'ja',
NL: 'nl',
Expand Down Expand Up @@ -54,6 +55,7 @@ const LOCALE_TO_LANGUAGE_STRING = {
[FULLY_SUPPORTED_LOCALES.ES]: 'Español',
[FULLY_SUPPORTED_LOCALES.FR]: 'Français',
[BETA_LOCALES.DE]: 'Deutsch',
[BETA_LOCALES.EL]: 'Ελληνικά',
[BETA_LOCALES.IT]: 'Italiano',
[BETA_LOCALES.JA]: '日本語',
[BETA_LOCALES.NL]: 'Nederlands',
Expand Down
30 changes: 29 additions & 1 deletion src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ type BaseHTMLEngineProviderProps = ChildrenProps & {
enableExperimentalBRCollapsing?: boolean;
};

/**
* Brand font family names allowed through react-native-render-html's whitelist.
* The library only keeps the first supported face from a CSS font stack, so without
* {@link HTML_FALLBACK_FONTS} the system fallbacks we add for non-Latin scripts are dropped.
*/
const HTML_SYSTEM_FONTS = Object.values(FontUtils.fontFamily.single).map((font) => font.fontFamily);

/**
* Remap brand faces to their full platform stacks (with system fallbacks).
*
* react-native-render-html's font-family validator returns only the first whitelisted
* face (e.g. "Expensify Neue"), which has no Greek/Cyrillic glyphs. Returning the full
* multi-font stack here restores emoji + system sans-serif fallbacks for those scripts.
*
* Extra keys beyond sans-serif/serif/monospace are supported at runtime by the library
* even though the published TS type is narrower.
*/
const CSS_SANS_SERIF = 'sans-serif';
const HTML_FALLBACK_FONTS = {
[CSS_SANS_SERIF]: FontUtils.fontFamily.platform.EXP_NEUE.fontFamily,
serif: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM.fontFamily,
monospace: FontUtils.fontFamily.platform.MONOSPACE.fontFamily,
[FontUtils.fontFamily.single.EXP_NEUE.fontFamily]: FontUtils.fontFamily.platform.EXP_NEUE.fontFamily,
[FontUtils.fontFamily.single.MONOSPACE.fontFamily]: FontUtils.fontFamily.platform.MONOSPACE.fontFamily,
[FontUtils.fontFamily.single.EXP_NEW_KANSAS_MEDIUM.fontFamily]: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM.fontFamily,
};

// We are using the explicit composite architecture for performance gains.
// Configuration for RenderHTML is handled in a top-level component providing
// context to RenderHTMLSource components. See https://git.io/JRcZb
Expand Down Expand Up @@ -218,7 +245,8 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
baseStyle={styles.webViewStyles.baseFontStyle}
tagsStyles={styles.webViewStyles.tagStyles}
enableCSSInlineProcessing={false}
systemFonts={Object.values(FontUtils.fontFamily.single).map((font) => font.fontFamily)}
systemFonts={HTML_SYSTEM_FONTS}
fallbackFonts={HTML_FALLBACK_FONTS}
htmlParserOptions={{
recognizeSelfClosing: true,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const APPLE_SIGN_IN_LOCALES = {

const MAP_EXFY_LOCALE_TO_APPLE_LOCALE: Record<Locale, ValueOf<typeof APPLE_SIGN_IN_LOCALES>> = {
[LOCALES.DE]: APPLE_SIGN_IN_LOCALES.de_DE,
[LOCALES.EL]: APPLE_SIGN_IN_LOCALES.el_GR,
[LOCALES.EN]: APPLE_SIGN_IN_LOCALES.en_US,
[LOCALES.ES]: APPLE_SIGN_IN_LOCALES.es_ES,
[LOCALES.FR]: APPLE_SIGN_IN_LOCALES.fr_FR,
Expand Down
12 changes: 12 additions & 0 deletions src/languages/IntlStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {setDefaultOptions} from 'date-fns';
import Onyx from 'react-native-onyx';

import type de from './de';
import type el from './el';
import type en from './en';
import type es from './es';
import type fr from './fr';
Expand Down Expand Up @@ -62,6 +63,17 @@ class IntlStore {
this.dateUtilsCache.set(LOCALES.DE, module.de);
}),
]),
[LOCALES.EL]: () =>
this.cache.has(LOCALES.EL)
? Promise.all([Promise.resolve(), Promise.resolve()])
: Promise.all([
import('./el').then((module: DynamicModule<typeof el>) => {
this.cache.set(LOCALES.EL, flattenObject(extractModuleDefaultExport(module)));
}),
import('date-fns/locale/el').then((module) => {
this.dateUtilsCache.set(LOCALES.EL, module.el);
}),
]),
[LOCALES.EN]: () =>
this.cache.has(LOCALES.EN)
? Promise.all([Promise.resolve(), Promise.resolve()])
Expand Down
Loading
Loading