Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]

### Fixed
- `check` run without a `localhero.json` recognises a regional English file (`en-US.json`, `en_US.yml`, `en-GB`) as the source. Before, it only matched a plain `en` and otherwise picked the language with the most keys, which in many JavaScript projects named after `en-US` meant checking every locale against, say, Arabic. US English wins when several English variants exist.
- `check` no longer flags a gettext plural form for using a placeholder only the plural source has ("Moved down 1 place." / "%(counter)s places"), or an ICU translation for saying the number with `#` instead of repeating `{n}`. Rails and i18next placeholders are still counted, so a dropped repeat like a second `%{days}` is still reported.
- `check --source <locale>` with no files for that locale now fails with a message instead of reporting 0 keys and 100% complete.
- `check` run without a `localhero.json` now finds locale folders below the project root, such as `packages/app/locales` or `apps/web/public/locales` in a monorepo, when the usual folder holds none. It checks the folder with the most languages and names any others it found, each checkable with `--path`. Folders under `node_modules`, `vendor`, build output, virtual environments and test fixtures are left out. Before, the run stopped with "No translation files found".
Expand Down
2 changes: 1 addition & 1 deletion src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ const SOURCE_REASONS: Record<DetectedSetup['reason'], string> = {
option: 'from --source',
gettext: 'its catalog is untranslated',
template: 'from the .pot template',
en: 'en is present',
en: 'English is present',
guessed: 'guessed from the most keys'
};

Expand Down
10 changes: 9 additions & 1 deletion src/utils/locale-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ export function looksLikeGettextSource(content: string): boolean {
return untranslated.length / entries.length >= GETTEXT_SOURCE_SHARE;
}

// en-US.json is the most common English file name in JavaScript projects.
function englishLocale(locales: string[]): string | null {
if (locales.includes('en')) return 'en';
const variants = locales.filter((locale) => /^en[-_]/i.test(locale));
return variants.find((locale) => /^en[-_]us$/i.test(locale)) ?? variants.sort()[0] ?? null;
}

export function chooseSourceLocale(input: {
explicit?: string;
locales: string[];
Expand All @@ -82,7 +89,8 @@ export function chooseSourceLocale(input: {
if (gettextSources.length === 1) return { locale: gettextSources[0], reason: 'gettext' };
if (hasTemplate) return { locale: 'en', reason: 'template' };
if (locales.length === 0) return null;
if (locales.includes('en')) return { locale: 'en', reason: 'en' };
const english = englishLocale(locales);
if (english) return { locale: english, reason: 'en' };

const [mostKeys] = [...locales].sort((a, b) => (keyCounts[b] ?? 0) - (keyCounts[a] ?? 0));
return { locale: mostKeys, reason: 'guessed' };
Expand Down
13 changes: 13 additions & 0 deletions tests/utils/locale-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ describe('chooseSourceLocale', () => {
expect(chooseSourceLocale({ locales, keyCounts, gettextSources: [], hasTemplate: false })).toEqual({ locale: 'en', reason: 'en' });
});

it('picks a regional English locale over the one with the most keys', () => {
const counts = { 'de-DE': 120, 'en-US': 100, 'ar-EG': 130 };
expect(chooseSourceLocale({ locales: ['de-DE', 'en-US', 'ar-EG'], keyCounts: counts, gettextSources: [], hasTemplate: false })).toEqual({ locale: 'en-US', reason: 'en' });
});

it.each([
[['en-GB', 'en-US', 'sv'], 'en-US'],
[['en_GB', 'en_US', 'sv'], 'en_US'],
[['en-GB', 'sv'], 'en-GB']
])('prefers US English among English variants in %j', (variants, expected) => {
expect(chooseSourceLocale({ locales: variants, keyCounts: {}, gettextSources: [], hasTemplate: false })).toEqual({ locale: expected, reason: 'en' });
});

it('guesses the locale with the most keys otherwise', () => {
expect(chooseSourceLocale({ locales: ['sv', 'de'], keyCounts, gettextSources: [], hasTemplate: false })).toEqual({ locale: 'sv', reason: 'guessed' });
});
Expand Down
Loading