From d0bef421864997e81de9399efd4737c04399f88b Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 26 Apr 2025 08:03:22 -0500 Subject: [PATCH] fix: Standardize locale codes and warn about unsupported ones (#1617) --- packages/i18n/src/__tests__/utils.test.ts | 34 ++++++++++++- packages/i18n/src/build.ts | 2 + packages/i18n/src/module.ts | 22 +++++++-- packages/i18n/src/supported-locales.ts | 58 +++++++++++++++++++++++ packages/i18n/src/utils.ts | 13 +++++ 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 packages/i18n/src/supported-locales.ts diff --git a/packages/i18n/src/__tests__/utils.test.ts b/packages/i18n/src/__tests__/utils.test.ts index e3ef657e..7c44bc46 100644 --- a/packages/i18n/src/__tests__/utils.test.ts +++ b/packages/i18n/src/__tests__/utils.test.ts @@ -1,10 +1,14 @@ import { describe, it, expect } from 'vitest'; import { ChromeMessage } from '../build'; -import { applyChromeMessagePlaceholders, getSubstitutionCount } from '../utils'; +import { + applyChromeMessagePlaceholders, + getSubstitutionCount, + standardizeLocale, +} from '../utils'; describe('Utils', () => { describe('applyChromeMessagePlaceholders', () => { - it('should return the combined stirng', () => { + it('should return the combined string', () => { const input = { message: 'Hello $username$, welcome to $appName$', placeholders: { @@ -60,4 +64,30 @@ describe('Utils', () => { expect(getSubstitutionCount('Hello $10')).toBe(1); }); }); + + describe('standardizeLocale', () => { + it('should convert two-letter locale codes to lowercase', () => { + expect(standardizeLocale('en')).toEqual('en'); + expect(standardizeLocale('EN')).toEqual('en'); + }); + + it('should convert locale code extensions to uppercase', () => { + expect(standardizeLocale('en_US')).toEqual('en_US'); + expect(standardizeLocale('en_us')).toEqual('en_US'); + expect(standardizeLocale('es_419')).toEqual('es_419'); + }); + + it('should convert dashes to underscores', () => { + expect(standardizeLocale('en_US')).toEqual('en_US'); + expect(standardizeLocale('en-US')).toEqual('en_US'); + }); + + it('should return the input string as-is for unknown formats', () => { + expect(standardizeLocale('en_USSS')).toEqual('en_USSS'); + expect(standardizeLocale('en-')).toEqual('en-'); + expect(standardizeLocale('------')).toEqual('------'); + expect(standardizeLocale('test')).toEqual('test'); + expect(standardizeLocale('hello-world')).toEqual('hello-world'); + }); + }); }); diff --git a/packages/i18n/src/build.ts b/packages/i18n/src/build.ts index 7ad7aef8..65144939 100644 --- a/packages/i18n/src/build.ts +++ b/packages/i18n/src/build.ts @@ -10,6 +10,8 @@ import { parseYAML, parseJSON5, parseTOML } from 'confbox'; import { dirname, extname } from 'node:path'; import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils'; +export { SUPPORTED_LOCALES } from './supported-locales'; + // // TYPES // diff --git a/packages/i18n/src/module.ts b/packages/i18n/src/module.ts index fdf9f29b..68c91489 100644 --- a/packages/i18n/src/module.ts +++ b/packages/i18n/src/module.ts @@ -16,12 +16,14 @@ import { generateChromeMessagesText, parseMessagesFile, generateTypeText, + SUPPORTED_LOCALES, } from './build'; import glob from 'fast-glob'; import { basename, extname, join, resolve } from 'node:path'; import { watch } from 'chokidar'; import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt'; import { writeFile } from 'node:fs/promises'; +import { standardizeLocale } from './utils'; export default defineWxtModule({ name: '@wxt-dev/i18n', @@ -46,10 +48,22 @@ export default defineWxtModule({ cwd: localesDir, absolute: true, }); - return files.map((file) => ({ - file, - locale: basename(file).replace(extname(file), ''), - })); + + const unsupportedLocales: string[] = []; + + const res = files.map((file) => { + const rawLocale = basename(file).replace(extname(file), ''); + const locale = standardizeLocale(rawLocale); + if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale); + return { file, locale }; + }); + + if (unsupportedLocales.length > 0) + wxt.logger.warn( + `Unsupported locales: [${unsupportedLocales.join(', ')}].\n\nWeb extensions only support a limited set of locales as described here: https://developer.chrome.com/docs/extensions/reference/api/i18n#locales`, + ); + + return res; }; const generateOutputJsonFiles = async (): Promise< diff --git a/packages/i18n/src/supported-locales.ts b/packages/i18n/src/supported-locales.ts new file mode 100644 index 00000000..9d05a6db --- /dev/null +++ b/packages/i18n/src/supported-locales.ts @@ -0,0 +1,58 @@ +/** From https://developer.chrome.com/docs/extensions/reference/api/i18n#locales */ +export const SUPPORTED_LOCALES = new Set([ + 'ar', + 'am', + 'bg', + 'bn', + 'ca', + 'cs', + 'da', + 'de', + 'el', + 'en', + 'en_AU', + 'en_GB', + 'en_US', + 'es', + 'es_419', + 'et', + 'fa', + 'fi', + 'fil', + 'fr', + 'gu', + 'he', + 'hi', + 'hr', + 'hu', + 'id', + 'it', + 'ja', + 'kn', + 'ko', + 'lt', + 'lv', + 'ml', + 'mr', + 'ms', + 'nl', + 'no', + 'pl', + 'pt_BR', + 'pt_PT', + 'ro', + 'ru', + 'sk', + 'sl', + 'sr', + 'sv', + 'sw', + 'ta', + 'te', + 'th', + 'tr', + 'uk', + 'vi', + 'zh_CN', + 'zh_TW', +]); diff --git a/packages/i18n/src/utils.ts b/packages/i18n/src/utils.ts index cb9b9634..6856c79a 100644 --- a/packages/i18n/src/utils.ts +++ b/packages/i18n/src/utils.ts @@ -21,3 +21,16 @@ export function getSubstitutionCount(message: string): number { } const MAX_SUBSTITUTIONS = 9; + +/** Given a string, standardize it to the format `xx_YY`. */ +export function standardizeLocale(locale: string): string { + if (locale.length === 2) return locale.toLowerCase(); + + const [is_match, prefix, suffix] = + locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? []; + if (is_match) { + return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`; + } + + return locale; +}