Skip to content
Open
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
24 changes: 0 additions & 24 deletions packages/i18n/src/__tests__/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,65 +47,41 @@ describe('I18n Types', () => {
describe('t', () => {
it('should only allow passing valid combinations of arguments', () => {
i18n.t('simple');
// @ts-expect-error
i18n.t('simple', []);
// @ts-expect-error
i18n.t('simple', ['one']);
// @ts-expect-error
i18n.t('simple', n);

i18n.t('simpleSub1', ['one']);
// @ts-expect-error
i18n.t('simpleSub1');
// @ts-expect-error
i18n.t('simpleSub1', []);
// @ts-expect-error
i18n.t('simpleSub1', ['one', 'two']);
// @ts-expect-error
i18n.t('simpleSub1', n);

i18n.t('simpleSub2', ['one', 'two']);
// @ts-expect-error
i18n.t('simpleSub2');
// @ts-expect-error
i18n.t('simpleSub2', ['one']);
// @ts-expect-error
i18n.t('simpleSub2', ['one', 'two', 'three']);
// @ts-expect-error
i18n.t('simpleSub2', n);

i18n.t('plural', n);
// @ts-expect-error
i18n.t('plural');
// @ts-expect-error
i18n.t('plural', []);
// @ts-expect-error
i18n.t('plural', ['one']);
// @ts-expect-error
i18n.t('plural', n, ['sub']);

i18n.t('pluralSub1', n);
i18n.t('pluralSub1', n, undefined);
i18n.t('pluralSub1', n, ['one']);
// @ts-expect-error
i18n.t('pluralSub1');
// @ts-expect-error
i18n.t('pluralSub1', ['one']);
// @ts-expect-error
i18n.t('pluralSub1', n, []);
// @ts-expect-error
i18n.t('pluralSub1', n, ['one', 'two']);

i18n.t('pluralSub2', n, ['one', 'two']);
// @ts-expect-error
i18n.t('pluralSub2');
// @ts-expect-error
i18n.t('pluralSub2', ['one', 'two']);
// @ts-expect-error
i18n.t('pluralSub2', n, ['one']);
// @ts-expect-error
i18n.t('pluralSub2', n, ['one', 'two', 'three']);
// @ts-expect-error
i18n.t('pluralSub2', n);
});
});
Expand Down
34 changes: 22 additions & 12 deletions packages/i18n/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { parseYAML, parseJSON5, parseTOML } from 'confbox';
import { parseJSON5, parseTOML, parseYAML } from 'confbox';
import { dirname, extname } from 'node:path';
import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';

Expand Down Expand Up @@ -46,6 +46,8 @@ export type ParsedMessage =

export type MessageFormat = 'JSON5' | 'YAML' | 'TOML';

type MessageType = Record<string, ChromeMessage> | Record<number | 'n', string>;

//
// CONSTANTS
//
Expand Down Expand Up @@ -94,7 +96,7 @@ const EXT_FORMATS_MAP: Record<string, MessageFormat> = {
'.toml': 'TOML',
};

const PARSERS: Record<MessageFormat, (text: string) => any> = {
const PARSERS: Record<MessageFormat, (text: string) => MessageType> = {
YAML: parseYAML,
JSON5: parseJSON5,
TOML: parseTOML,
Expand All @@ -118,6 +120,7 @@ export async function parseMessagesFile(
): Promise<ParsedMessage[]> {
const text = await readFile(file, 'utf8');
const ext = extname(file).toLowerCase();

return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5');
}

Expand All @@ -134,7 +137,7 @@ export function parseMessagesText(
/**
* Given the JS object form of a raw messages file, extract the messages.
*/
export function parseMessagesObject(object: any): ParsedMessage[] {
export function parseMessagesObject(object: MessageType): ParsedMessage[] {
return _parseMessagesObject(
[],
{
Expand All @@ -147,7 +150,7 @@ export function parseMessagesObject(object: any): ParsedMessage[] {

function _parseMessagesObject(
path: string[],
object: any,
object: MessageType,
depth: number,
): ParsedMessage[] {
switch (typeof object) {
Expand All @@ -168,54 +171,61 @@ function _parseMessagesObject(
];
}
case 'object':
if ([null, undefined].includes(object)) {
if (object === null || object === undefined) {
throw new Error(
`Messages file should not contain \`${object}\` (found at "${path.join('.')}")`,
);
}

if (Array.isArray(object))
return object.flatMap((item, i) =>
_parseMessagesObject(path.concat(String(i)), item, depth + 1),
);

if (isPluralMessage(object)) {
const message = Object.values(object).join('|');
const substitutions = getSubstitutionCount(message);

return [
{
type: 'plural',
key: path,
substitutions,
plurals: object,
plurals: object as Record<string, string>,
},
];
}

if (depth === 1 && isChromeMessage(object)) {
const message = applyChromeMessagePlaceholders(object);
const message = applyChromeMessagePlaceholders(
object as unknown as ChromeMessage,
);
const substitutions = getSubstitutionCount(message);

return [
{
type: 'chrome',
key: path,
substitutions,
...object,
...(object as unknown as ChromeMessage),
},
];
}
return Object.entries(object).flatMap(([key, value]) =>
_parseMessagesObject(path.concat(key), value, depth + 1),
_parseMessagesObject(path.concat(key), value as MessageType, depth + 1),
);
default:
throw Error(`"Could not parse object of type "${typeof object}"`);
}
}

function isPluralMessage(object: any): object is Record<number | 'n', string> {
function isPluralMessage(object: MessageType) {
return Object.keys(object).every(
(key) => key === 'n' || isFinite(Number(key)),
);
}

function isChromeMessage(object: any): object is ChromeMessage {
function isChromeMessage(object: MessageType) {
return Object.keys(object).every((key) =>
ALLOWED_CHROME_MESSAGE_KEYS.has(key),
);
Expand All @@ -227,7 +237,7 @@ function isChromeMessage(object: any): object is ChromeMessage {

export function generateTypeText(messages: ParsedMessage[]): string {
const renderMessageEntry = (message: ParsedMessage): string => {
// Use . for deep keys at runtime and types
// Use '.' for deep keys at runtime and types
const key = message.key.join('.');

const features = [
Expand Down
17 changes: 5 additions & 12 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
/**
* @module @wxt-dev/i18n
*/
import {
I18nStructure,
DefaultI18nStructure,
I18n,
Substitution,
} from './types';
import { I18nStructure, I18n, Substitution } from './types';
import { browser } from '@wxt-dev/browser';

export function createI18n<
T extends I18nStructure = DefaultI18nStructure,
>(): I18n<T> {
const t = (key: string, ...args: any[]) => {
export function createI18n<T extends I18nStructure>(): I18n<T> {
const t = ((key, ...args) => {
// Resolve args
let sub: Substitution[] | undefined;
let count: number | undefined;
Expand Down Expand Up @@ -64,7 +57,7 @@ export function createI18n<
default:
throw Error('Unknown plural formatting');
}
};
}) as I18n<T>['t'];

return { t } as I18n<T>;
return { t };
}
24 changes: 15 additions & 9 deletions packages/i18n/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,23 @@ export type I18nStructure = {
[K: string]: I18nFeatures;
};

export type DefaultI18nStructure = {
[K: string]: any;
type DefaultTFunction<TKeys extends string> = {
(key: TKeys): string;
(key: TKeys, substitutions?: string[]): string;
(key: TKeys, n: number): string;
(key: TKeys, n: number, substitutions?: string[]): string;
};

export interface I18n<T extends I18nStructure> {
t: T extends I18nStructure
? DefaultTFunction<keyof T & string>
: TFunction<Extract<T, I18nStructure>>;
}

// prettier-ignore
export type SubstitutionTuple<T extends SubstitutionCount> =
T extends 1 ? [$1: Substitution]
T extends 0 ? []
: T extends 1 ? [$1: Substitution]
: T extends 2 ? [$1: Substitution, $2: Substitution]
: T extends 3 ? [$1: Substitution, $2: Substitution, $3: Substitution]
: T extends 4 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution]
Expand All @@ -22,7 +32,7 @@ export type SubstitutionTuple<T extends SubstitutionCount> =
: T extends 7 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution]
: T extends 8 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution]
: T extends 9 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution, $9: Substitution]
: never
: []

export type TFunction<T extends I18nStructure> = {
// Non-plural, no substitutions
Expand All @@ -31,7 +41,7 @@ export type TFunction<T extends I18nStructure> = {
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0 } ? P : never; }[keyof T],
): string;

// Non-plural with substitutions
// Non-plural with optional substitutions
<K extends keyof T>(
// prettier-ignore
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? P : never; }[keyof T],
Expand Down Expand Up @@ -66,10 +76,6 @@ export type TFunction<T extends I18nStructure> = {
): string;
};

export interface I18n<T extends DefaultI18nStructure> {
t: TFunction<T>;
}

export type Substitution = string | number;

type SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
Loading