refactor: fully typed locales (#13033)
* refactor: fully typed locales * refactor: hide parameterized locale strings from type data in ts access * refactor: missing assertions * docs: annotation
This commit is contained in:
parent
c1019a006b
commit
43401210c3
|
@ -6,54 +6,171 @@ import ts from 'typescript';
|
|||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const parameterRegExp = /\{(\w+)\}/g;
|
||||
|
||||
function createMemberType(item) {
|
||||
if (typeof item !== 'string') {
|
||||
return ts.factory.createTypeLiteralNode(createMembers(item));
|
||||
}
|
||||
const parameters = Array.from(
|
||||
item.matchAll(parameterRegExp),
|
||||
([, parameter]) => parameter,
|
||||
);
|
||||
if (!parameters.length) {
|
||||
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
||||
}
|
||||
return ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createUnionTypeNode(
|
||||
parameters.map((parameter) =>
|
||||
ts.factory.createLiteralTypeNode(
|
||||
ts.factory.createStringLiteral(parameter),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function createMembers(record) {
|
||||
return Object.entries(record)
|
||||
.map(([k, v]) => ts.factory.createPropertySignature(
|
||||
return Object.entries(record).map(([k, v]) =>
|
||||
ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createStringLiteral(k),
|
||||
undefined,
|
||||
typeof v === 'string'
|
||||
? ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
|
||||
: ts.factory.createTypeLiteralNode(createMembers(v)),
|
||||
));
|
||||
createMemberType(v),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default function generateDTS() {
|
||||
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
|
||||
const members = createMembers(locale);
|
||||
const elements = [
|
||||
ts.factory.createVariableStatement(
|
||||
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('kParameters'),
|
||||
undefined,
|
||||
ts.factory.createTypeOperatorNode(
|
||||
ts.SyntaxKind.UniqueKeyword,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.SymbolKeyword),
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const,
|
||||
),
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[
|
||||
ts.factory.createTypeParameterDeclaration(
|
||||
undefined,
|
||||
ts.factory.createIdentifier('T'),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('string'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createPropertySignature(
|
||||
undefined,
|
||||
ts.factory.createComputedPropertyName(
|
||||
ts.factory.createIdentifier('kParameters'),
|
||||
),
|
||||
undefined,
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('T'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createIndexSignature(
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('_'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.factory.createUnionTypeNode([
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ParameterizedString'),
|
||||
[ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)],
|
||||
),
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
ts.factory.createInterfaceDeclaration(
|
||||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [
|
||||
ts.factory.createExpressionWithTypeArguments(
|
||||
ts.factory.createIdentifier('ILocale'),
|
||||
undefined,
|
||||
),
|
||||
]),
|
||||
],
|
||||
members,
|
||||
),
|
||||
ts.factory.createVariableStatement(
|
||||
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('locales'),
|
||||
undefined,
|
||||
ts.factory.createTypeLiteralNode([ts.factory.createIndexSignature(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
ts.factory.createIdentifier('locales'),
|
||||
undefined,
|
||||
[ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('lang'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
||||
undefined,
|
||||
)],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
)]),
|
||||
undefined,
|
||||
)],
|
||||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||
ts.factory.createTypeLiteralNode([
|
||||
ts.factory.createIndexSignature(
|
||||
undefined,
|
||||
[
|
||||
ts.factory.createParameterDeclaration(
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createIdentifier('lang'),
|
||||
undefined,
|
||||
ts.factory.createKeywordTypeNode(
|
||||
ts.SyntaxKind.StringKeyword,
|
||||
),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.factory.createTypeReferenceNode(
|
||||
ts.factory.createIdentifier('Locale'),
|
||||
undefined,
|
||||
),
|
||||
),
|
||||
]),
|
||||
undefined,
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const,
|
||||
),
|
||||
),
|
||||
ts.factory.createFunctionDeclaration(
|
||||
|
@ -70,16 +187,28 @@ export default function generateDTS() {
|
|||
),
|
||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||
];
|
||||
const printed = ts.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
}).printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
ts.factory.createNodeArray(elements),
|
||||
ts.createSourceFile('index.d.ts', '', ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS),
|
||||
);
|
||||
const printed = ts
|
||||
.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
})
|
||||
.printList(
|
||||
ts.ListFormat.MultiLine,
|
||||
ts.factory.createNodeArray(elements),
|
||||
ts.createSourceFile(
|
||||
'index.d.ts',
|
||||
'',
|
||||
ts.ScriptTarget.ESNext,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
),
|
||||
);
|
||||
|
||||
fs.writeFileSync(`${__dirname}/index.d.ts`, `/* eslint-disable */
|
||||
fs.writeFileSync(
|
||||
`${__dirname}/index.d.ts`,
|
||||
`/* eslint-disable */
|
||||
// This file is generated by locales/generateDTS.js
|
||||
// Do not edit this file directly.
|
||||
${printed}`, 'utf-8');
|
||||
${printed}`,
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
/* eslint-disable */
|
||||
// This file is generated by locales/generateDTS.js
|
||||
// Do not edit this file directly.
|
||||
export interface Locale {
|
||||
declare const kParameters: unique symbol;
|
||||
export interface ParameterizedString<T extends string> {
|
||||
[kParameters]: T;
|
||||
}
|
||||
export interface ILocale {
|
||||
[_: string]: string | ParameterizedString<string> | ILocale;
|
||||
}
|
||||
export interface Locale extends ILocale {
|
||||
"_lang_": string;
|
||||
"headlineMisskey": string;
|
||||
"introMisskey": string;
|
||||
"poweredByMisskeyDescription": string;
|
||||
"monthAndDay": string;
|
||||
"poweredByMisskeyDescription": ParameterizedString<"name">;
|
||||
"monthAndDay": ParameterizedString<"month" | "day">;
|
||||
"search": string;
|
||||
"notifications": string;
|
||||
"username": string;
|
||||
|
@ -18,7 +25,7 @@ export interface Locale {
|
|||
"cancel": string;
|
||||
"noThankYou": string;
|
||||
"enterUsername": string;
|
||||
"renotedBy": string;
|
||||
"renotedBy": ParameterizedString<"user">;
|
||||
"noNotes": string;
|
||||
"noNotifications": string;
|
||||
"instance": string;
|
||||
|
@ -78,8 +85,8 @@ export interface Locale {
|
|||
"export": string;
|
||||
"files": string;
|
||||
"download": string;
|
||||
"driveFileDeleteConfirm": string;
|
||||
"unfollowConfirm": string;
|
||||
"driveFileDeleteConfirm": ParameterizedString<"name">;
|
||||
"unfollowConfirm": ParameterizedString<"name">;
|
||||
"exportRequested": string;
|
||||
"importRequested": string;
|
||||
"lists": string;
|
||||
|
@ -183,9 +190,9 @@ export interface Locale {
|
|||
"wallpaper": string;
|
||||
"setWallpaper": string;
|
||||
"removeWallpaper": string;
|
||||
"searchWith": string;
|
||||
"searchWith": ParameterizedString<"q">;
|
||||
"youHaveNoLists": string;
|
||||
"followConfirm": string;
|
||||
"followConfirm": ParameterizedString<"name">;
|
||||
"proxyAccount": string;
|
||||
"proxyAccountDescription": string;
|
||||
"host": string;
|
||||
|
@ -208,7 +215,7 @@ export interface Locale {
|
|||
"software": string;
|
||||
"version": string;
|
||||
"metadata": string;
|
||||
"withNFiles": string;
|
||||
"withNFiles": ParameterizedString<"n">;
|
||||
"monitor": string;
|
||||
"jobQueue": string;
|
||||
"cpuAndMemory": string;
|
||||
|
@ -237,7 +244,7 @@ export interface Locale {
|
|||
"processing": string;
|
||||
"preview": string;
|
||||
"default": string;
|
||||
"defaultValueIs": string;
|
||||
"defaultValueIs": ParameterizedString<"value">;
|
||||
"noCustomEmojis": string;
|
||||
"noJobs": string;
|
||||
"federating": string;
|
||||
|
@ -266,8 +273,8 @@ export interface Locale {
|
|||
"imageUrl": string;
|
||||
"remove": string;
|
||||
"removed": string;
|
||||
"removeAreYouSure": string;
|
||||
"deleteAreYouSure": string;
|
||||
"removeAreYouSure": ParameterizedString<"x">;
|
||||
"deleteAreYouSure": ParameterizedString<"x">;
|
||||
"resetAreYouSure": string;
|
||||
"areYouSure": string;
|
||||
"saved": string;
|
||||
|
@ -285,8 +292,8 @@ export interface Locale {
|
|||
"messageRead": string;
|
||||
"noMoreHistory": string;
|
||||
"startMessaging": string;
|
||||
"nUsersRead": string;
|
||||
"agreeTo": string;
|
||||
"nUsersRead": ParameterizedString<"n">;
|
||||
"agreeTo": ParameterizedString<"0">;
|
||||
"agree": string;
|
||||
"agreeBelow": string;
|
||||
"basicNotesBeforeCreateAccount": string;
|
||||
|
@ -298,7 +305,7 @@ export interface Locale {
|
|||
"images": string;
|
||||
"image": string;
|
||||
"birthday": string;
|
||||
"yearsOld": string;
|
||||
"yearsOld": ParameterizedString<"age">;
|
||||
"registeredDate": string;
|
||||
"location": string;
|
||||
"theme": string;
|
||||
|
@ -353,9 +360,9 @@ export interface Locale {
|
|||
"thisYear": string;
|
||||
"thisMonth": string;
|
||||
"today": string;
|
||||
"dayX": string;
|
||||
"monthX": string;
|
||||
"yearX": string;
|
||||
"dayX": ParameterizedString<"day">;
|
||||
"monthX": ParameterizedString<"month">;
|
||||
"yearX": ParameterizedString<"year">;
|
||||
"pages": string;
|
||||
"integration": string;
|
||||
"connectService": string;
|
||||
|
@ -420,7 +427,7 @@ export interface Locale {
|
|||
"recentlyUpdatedUsers": string;
|
||||
"recentlyRegisteredUsers": string;
|
||||
"recentlyDiscoveredUsers": string;
|
||||
"exploreUsersCount": string;
|
||||
"exploreUsersCount": ParameterizedString<"count">;
|
||||
"exploreFediverse": string;
|
||||
"popularTags": string;
|
||||
"userList": string;
|
||||
|
@ -437,16 +444,16 @@ export interface Locale {
|
|||
"moderationNote": string;
|
||||
"addModerationNote": string;
|
||||
"moderationLogs": string;
|
||||
"nUsersMentioned": string;
|
||||
"nUsersMentioned": ParameterizedString<"n">;
|
||||
"securityKeyAndPasskey": string;
|
||||
"securityKey": string;
|
||||
"lastUsed": string;
|
||||
"lastUsedAt": string;
|
||||
"lastUsedAt": ParameterizedString<"t">;
|
||||
"unregister": string;
|
||||
"passwordLessLogin": string;
|
||||
"passwordLessLoginDescription": string;
|
||||
"resetPassword": string;
|
||||
"newPasswordIs": string;
|
||||
"newPasswordIs": ParameterizedString<"password">;
|
||||
"reduceUiAnimation": string;
|
||||
"share": string;
|
||||
"notFound": string;
|
||||
|
@ -466,7 +473,7 @@ export interface Locale {
|
|||
"enable": string;
|
||||
"next": string;
|
||||
"retype": string;
|
||||
"noteOf": string;
|
||||
"noteOf": ParameterizedString<"user">;
|
||||
"quoteAttached": string;
|
||||
"quoteQuestion": string;
|
||||
"noMessagesYet": string;
|
||||
|
@ -486,12 +493,12 @@ export interface Locale {
|
|||
"strongPassword": string;
|
||||
"passwordMatched": string;
|
||||
"passwordNotMatched": string;
|
||||
"signinWith": string;
|
||||
"signinWith": ParameterizedString<"x">;
|
||||
"signinFailed": string;
|
||||
"or": string;
|
||||
"language": string;
|
||||
"uiLanguage": string;
|
||||
"aboutX": string;
|
||||
"aboutX": ParameterizedString<"x">;
|
||||
"emojiStyle": string;
|
||||
"native": string;
|
||||
"disableDrawer": string;
|
||||
|
@ -509,7 +516,7 @@ export interface Locale {
|
|||
"regenerate": string;
|
||||
"fontSize": string;
|
||||
"mediaListWithOneImageAppearance": string;
|
||||
"limitTo": string;
|
||||
"limitTo": ParameterizedString<"x">;
|
||||
"noFollowRequests": string;
|
||||
"openImageInNewTab": string;
|
||||
"dashboard": string;
|
||||
|
@ -587,7 +594,7 @@ export interface Locale {
|
|||
"deleteAllFiles": string;
|
||||
"deleteAllFilesConfirm": string;
|
||||
"removeAllFollowing": string;
|
||||
"removeAllFollowingDescription": string;
|
||||
"removeAllFollowingDescription": ParameterizedString<"host">;
|
||||
"userSuspended": string;
|
||||
"userSilenced": string;
|
||||
"yourAccountSuspendedTitle": string;
|
||||
|
@ -658,9 +665,9 @@ export interface Locale {
|
|||
"wordMute": string;
|
||||
"hardWordMute": string;
|
||||
"regexpError": string;
|
||||
"regexpErrorDescription": string;
|
||||
"regexpErrorDescription": ParameterizedString<"tab" | "line">;
|
||||
"instanceMute": string;
|
||||
"userSaysSomething": string;
|
||||
"userSaysSomething": ParameterizedString<"name">;
|
||||
"makeActive": string;
|
||||
"display": string;
|
||||
"copy": string;
|
||||
|
@ -686,7 +693,7 @@ export interface Locale {
|
|||
"abuseReports": string;
|
||||
"reportAbuse": string;
|
||||
"reportAbuseRenote": string;
|
||||
"reportAbuseOf": string;
|
||||
"reportAbuseOf": ParameterizedString<"name">;
|
||||
"fillAbuseReportDescription": string;
|
||||
"abuseReported": string;
|
||||
"reporter": string;
|
||||
|
@ -701,7 +708,7 @@ export interface Locale {
|
|||
"defaultNavigationBehaviour": string;
|
||||
"editTheseSettingsMayBreakAccount": string;
|
||||
"instanceTicker": string;
|
||||
"waitingFor": string;
|
||||
"waitingFor": ParameterizedString<"x">;
|
||||
"random": string;
|
||||
"system": string;
|
||||
"switchUi": string;
|
||||
|
@ -711,10 +718,10 @@ export interface Locale {
|
|||
"optional": string;
|
||||
"createNewClip": string;
|
||||
"unclip": string;
|
||||
"confirmToUnclipAlreadyClippedNote": string;
|
||||
"confirmToUnclipAlreadyClippedNote": ParameterizedString<"name">;
|
||||
"public": string;
|
||||
"private": string;
|
||||
"i18nInfo": string;
|
||||
"i18nInfo": ParameterizedString<"link">;
|
||||
"manageAccessTokens": string;
|
||||
"accountInfo": string;
|
||||
"notesCount": string;
|
||||
|
@ -764,9 +771,9 @@ export interface Locale {
|
|||
"needReloadToApply": string;
|
||||
"showTitlebar": string;
|
||||
"clearCache": string;
|
||||
"onlineUsersCount": string;
|
||||
"nUsers": string;
|
||||
"nNotes": string;
|
||||
"onlineUsersCount": ParameterizedString<"n">;
|
||||
"nUsers": ParameterizedString<"n">;
|
||||
"nNotes": ParameterizedString<"n">;
|
||||
"sendErrorReports": string;
|
||||
"sendErrorReportsDescription": string;
|
||||
"myTheme": string;
|
||||
|
@ -798,7 +805,7 @@ export interface Locale {
|
|||
"publish": string;
|
||||
"inChannelSearch": string;
|
||||
"useReactionPickerForContextMenu": string;
|
||||
"typingUsers": string;
|
||||
"typingUsers": ParameterizedString<"users">;
|
||||
"jumpToSpecifiedDate": string;
|
||||
"showingPastTimeline": string;
|
||||
"clear": string;
|
||||
|
@ -865,7 +872,7 @@ export interface Locale {
|
|||
"misskeyUpdated": string;
|
||||
"whatIsNew": string;
|
||||
"translate": string;
|
||||
"translatedFrom": string;
|
||||
"translatedFrom": ParameterizedString<"x">;
|
||||
"accountDeletionInProgress": string;
|
||||
"usernameInfo": string;
|
||||
"aiChanMode": string;
|
||||
|
@ -896,11 +903,11 @@ export interface Locale {
|
|||
"continueThread": string;
|
||||
"deleteAccountConfirm": string;
|
||||
"incorrectPassword": string;
|
||||
"voteConfirm": string;
|
||||
"voteConfirm": ParameterizedString<"choice">;
|
||||
"hide": string;
|
||||
"useDrawerReactionPickerForMobile": string;
|
||||
"welcomeBackWithName": string;
|
||||
"clickToFinishEmailVerification": string;
|
||||
"welcomeBackWithName": ParameterizedString<"name">;
|
||||
"clickToFinishEmailVerification": ParameterizedString<"ok">;
|
||||
"overridedDeviceKind": string;
|
||||
"smartphone": string;
|
||||
"tablet": string;
|
||||
|
@ -928,8 +935,8 @@ export interface Locale {
|
|||
"cropYes": string;
|
||||
"cropNo": string;
|
||||
"file": string;
|
||||
"recentNHours": string;
|
||||
"recentNDays": string;
|
||||
"recentNHours": ParameterizedString<"n">;
|
||||
"recentNDays": ParameterizedString<"n">;
|
||||
"noEmailServerWarning": string;
|
||||
"thereIsUnresolvedAbuseReportWarning": string;
|
||||
"recommended": string;
|
||||
|
@ -938,7 +945,7 @@ export interface Locale {
|
|||
"driveCapOverrideCaption": string;
|
||||
"requireAdminForView": string;
|
||||
"isSystemAccount": string;
|
||||
"typeToConfirm": string;
|
||||
"typeToConfirm": ParameterizedString<"x">;
|
||||
"deleteAccount": string;
|
||||
"document": string;
|
||||
"numberOfPageCache": string;
|
||||
|
@ -992,7 +999,7 @@ export interface Locale {
|
|||
"neverShow": string;
|
||||
"remindMeLater": string;
|
||||
"didYouLikeMisskey": string;
|
||||
"pleaseDonate": string;
|
||||
"pleaseDonate": ParameterizedString<"host">;
|
||||
"roles": string;
|
||||
"role": string;
|
||||
"noRole": string;
|
||||
|
@ -1090,7 +1097,7 @@ export interface Locale {
|
|||
"preservedUsernamesDescription": string;
|
||||
"createNoteFromTheFile": string;
|
||||
"archive": string;
|
||||
"channelArchiveConfirmTitle": string;
|
||||
"channelArchiveConfirmTitle": ParameterizedString<"name">;
|
||||
"channelArchiveConfirmDescription": string;
|
||||
"thisChannelArchived": string;
|
||||
"displayOfNote": string;
|
||||
|
@ -1120,8 +1127,8 @@ export interface Locale {
|
|||
"createCount": string;
|
||||
"inviteCodeCreated": string;
|
||||
"inviteLimitExceeded": string;
|
||||
"createLimitRemaining": string;
|
||||
"inviteLimitResetCycle": string;
|
||||
"createLimitRemaining": ParameterizedString<"limit">;
|
||||
"inviteLimitResetCycle": ParameterizedString<"time" | "limit">;
|
||||
"expirationDate": string;
|
||||
"noExpirationDate": string;
|
||||
"inviteCodeUsedAt": string;
|
||||
|
@ -1134,7 +1141,7 @@ export interface Locale {
|
|||
"expired": string;
|
||||
"doYouAgree": string;
|
||||
"beSureToReadThisAsItIsImportant": string;
|
||||
"iHaveReadXCarefullyAndAgree": string;
|
||||
"iHaveReadXCarefullyAndAgree": ParameterizedString<"x">;
|
||||
"dialog": string;
|
||||
"icon": string;
|
||||
"forYou": string;
|
||||
|
@ -1189,7 +1196,7 @@ export interface Locale {
|
|||
"doReaction": string;
|
||||
"code": string;
|
||||
"reloadRequiredToApplySettings": string;
|
||||
"remainingN": string;
|
||||
"remainingN": ParameterizedString<"n">;
|
||||
"overwriteContentConfirm": string;
|
||||
"seasonalScreenEffect": string;
|
||||
"decorate": string;
|
||||
|
@ -1202,7 +1209,7 @@ export interface Locale {
|
|||
"replay": string;
|
||||
"replaying": string;
|
||||
"ranking": string;
|
||||
"lastNDays": string;
|
||||
"lastNDays": ParameterizedString<"n">;
|
||||
"backToTitle": string;
|
||||
"enableHorizontalSwipe": string;
|
||||
"_bubbleGame": {
|
||||
|
@ -1221,7 +1228,7 @@ export interface Locale {
|
|||
"end": string;
|
||||
"tooManyActiveAnnouncementDescription": string;
|
||||
"readConfirmTitle": string;
|
||||
"readConfirmText": string;
|
||||
"readConfirmText": ParameterizedString<"title">;
|
||||
"shouldNotBeUsedToPresentPermanentInfo": string;
|
||||
"dialogAnnouncementUxWarn": string;
|
||||
"silence": string;
|
||||
|
@ -1236,10 +1243,10 @@ export interface Locale {
|
|||
"theseSettingsCanEditLater": string;
|
||||
"youCanEditMoreSettingsInSettingsPageLater": string;
|
||||
"followUsers": string;
|
||||
"pushNotificationDescription": string;
|
||||
"pushNotificationDescription": ParameterizedString<"name">;
|
||||
"initialAccountSettingCompleted": string;
|
||||
"haveFun": string;
|
||||
"youCanContinueTutorial": string;
|
||||
"haveFun": ParameterizedString<"name">;
|
||||
"youCanContinueTutorial": ParameterizedString<"name">;
|
||||
"startTutorial": string;
|
||||
"skipAreYouSure": string;
|
||||
"laterAreYouSure": string;
|
||||
|
@ -1277,7 +1284,7 @@ export interface Locale {
|
|||
"social": string;
|
||||
"global": string;
|
||||
"description2": string;
|
||||
"description3": string;
|
||||
"description3": ParameterizedString<"link">;
|
||||
};
|
||||
"_postNote": {
|
||||
"title": string;
|
||||
|
@ -1315,7 +1322,7 @@ export interface Locale {
|
|||
};
|
||||
"_done": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
"description": ParameterizedString<"link">;
|
||||
};
|
||||
};
|
||||
"_timelineDescription": {
|
||||
|
@ -1329,10 +1336,10 @@ export interface Locale {
|
|||
};
|
||||
"_serverSettings": {
|
||||
"iconUrl": string;
|
||||
"appIconDescription": string;
|
||||
"appIconDescription": ParameterizedString<"host">;
|
||||
"appIconUsageExample": string;
|
||||
"appIconStyleRecommendation": string;
|
||||
"appIconResolutionMustBe": string;
|
||||
"appIconResolutionMustBe": ParameterizedString<"resolution">;
|
||||
"manifestJsonOverride": string;
|
||||
"shortName": string;
|
||||
"shortNameDescription": string;
|
||||
|
@ -1343,7 +1350,7 @@ export interface Locale {
|
|||
"_accountMigration": {
|
||||
"moveFrom": string;
|
||||
"moveFromSub": string;
|
||||
"moveFromLabel": string;
|
||||
"moveFromLabel": ParameterizedString<"n">;
|
||||
"moveFromDescription": string;
|
||||
"moveTo": string;
|
||||
"moveToLabel": string;
|
||||
|
@ -1351,7 +1358,7 @@ export interface Locale {
|
|||
"moveAccountDescription": string;
|
||||
"moveAccountHowTo": string;
|
||||
"startMigration": string;
|
||||
"migrationConfirm": string;
|
||||
"migrationConfirm": ParameterizedString<"account">;
|
||||
"movedAndCannotBeUndone": string;
|
||||
"postMigrationNote": string;
|
||||
"movedTo": string;
|
||||
|
@ -1793,7 +1800,7 @@ export interface Locale {
|
|||
"_signup": {
|
||||
"almostThere": string;
|
||||
"emailAddressInfo": string;
|
||||
"emailSent": string;
|
||||
"emailSent": ParameterizedString<"email">;
|
||||
};
|
||||
"_accountDelete": {
|
||||
"accountDelete": string;
|
||||
|
@ -1846,14 +1853,14 @@ export interface Locale {
|
|||
"save": string;
|
||||
"inputName": string;
|
||||
"cannotSave": string;
|
||||
"nameAlreadyExists": string;
|
||||
"applyConfirm": string;
|
||||
"saveConfirm": string;
|
||||
"deleteConfirm": string;
|
||||
"renameConfirm": string;
|
||||
"nameAlreadyExists": ParameterizedString<"name">;
|
||||
"applyConfirm": ParameterizedString<"name">;
|
||||
"saveConfirm": ParameterizedString<"name">;
|
||||
"deleteConfirm": ParameterizedString<"name">;
|
||||
"renameConfirm": ParameterizedString<"old" | "new">;
|
||||
"noBackups": string;
|
||||
"createdAt": string;
|
||||
"updatedAt": string;
|
||||
"createdAt": ParameterizedString<"date" | "time">;
|
||||
"updatedAt": ParameterizedString<"date" | "time">;
|
||||
"cannotLoad": string;
|
||||
"invalidFile": string;
|
||||
};
|
||||
|
@ -1898,8 +1905,8 @@ export interface Locale {
|
|||
"featured": string;
|
||||
"owned": string;
|
||||
"following": string;
|
||||
"usersCount": string;
|
||||
"notesCount": string;
|
||||
"usersCount": ParameterizedString<"n">;
|
||||
"notesCount": ParameterizedString<"n">;
|
||||
"nameAndDescription": string;
|
||||
"nameOnly": string;
|
||||
"allowRenoteToExternal": string;
|
||||
|
@ -1927,7 +1934,7 @@ export interface Locale {
|
|||
"manage": string;
|
||||
"code": string;
|
||||
"description": string;
|
||||
"installed": string;
|
||||
"installed": ParameterizedString<"name">;
|
||||
"installedThemes": string;
|
||||
"builtinThemes": string;
|
||||
"alreadyInstalled": string;
|
||||
|
@ -1950,7 +1957,7 @@ export interface Locale {
|
|||
"lighten": string;
|
||||
"inputConstantName": string;
|
||||
"importInfo": string;
|
||||
"deleteConstantConfirm": string;
|
||||
"deleteConstantConfirm": ParameterizedString<"const">;
|
||||
"keys": {
|
||||
"accent": string;
|
||||
"bg": string;
|
||||
|
@ -2013,23 +2020,23 @@ export interface Locale {
|
|||
"_ago": {
|
||||
"future": string;
|
||||
"justNow": string;
|
||||
"secondsAgo": string;
|
||||
"minutesAgo": string;
|
||||
"hoursAgo": string;
|
||||
"daysAgo": string;
|
||||
"weeksAgo": string;
|
||||
"monthsAgo": string;
|
||||
"yearsAgo": string;
|
||||
"secondsAgo": ParameterizedString<"n">;
|
||||
"minutesAgo": ParameterizedString<"n">;
|
||||
"hoursAgo": ParameterizedString<"n">;
|
||||
"daysAgo": ParameterizedString<"n">;
|
||||
"weeksAgo": ParameterizedString<"n">;
|
||||
"monthsAgo": ParameterizedString<"n">;
|
||||
"yearsAgo": ParameterizedString<"n">;
|
||||
"invalid": string;
|
||||
};
|
||||
"_timeIn": {
|
||||
"seconds": string;
|
||||
"minutes": string;
|
||||
"hours": string;
|
||||
"days": string;
|
||||
"weeks": string;
|
||||
"months": string;
|
||||
"years": string;
|
||||
"seconds": ParameterizedString<"n">;
|
||||
"minutes": ParameterizedString<"n">;
|
||||
"hours": ParameterizedString<"n">;
|
||||
"days": ParameterizedString<"n">;
|
||||
"weeks": ParameterizedString<"n">;
|
||||
"months": ParameterizedString<"n">;
|
||||
"years": ParameterizedString<"n">;
|
||||
};
|
||||
"_time": {
|
||||
"second": string;
|
||||
|
@ -2040,7 +2047,7 @@ export interface Locale {
|
|||
"_2fa": {
|
||||
"alreadyRegistered": string;
|
||||
"registerTOTP": string;
|
||||
"step1": string;
|
||||
"step1": ParameterizedString<"a" | "b">;
|
||||
"step2": string;
|
||||
"step2Click": string;
|
||||
"step2Uri": string;
|
||||
|
@ -2055,7 +2062,7 @@ export interface Locale {
|
|||
"securityKeyName": string;
|
||||
"tapSecurityKey": string;
|
||||
"removeKey": string;
|
||||
"removeKeyConfirm": string;
|
||||
"removeKeyConfirm": ParameterizedString<"name">;
|
||||
"whyTOTPOnlyRenew": string;
|
||||
"renewTOTP": string;
|
||||
"renewTOTPConfirm": string;
|
||||
|
@ -2156,9 +2163,9 @@ export interface Locale {
|
|||
};
|
||||
"_auth": {
|
||||
"shareAccessTitle": string;
|
||||
"shareAccess": string;
|
||||
"shareAccess": ParameterizedString<"name">;
|
||||
"shareAccessAsk": string;
|
||||
"permission": string;
|
||||
"permission": ParameterizedString<"name">;
|
||||
"permissionAsk": string;
|
||||
"pleaseGoBack": string;
|
||||
"callback": string;
|
||||
|
@ -2217,12 +2224,12 @@ export interface Locale {
|
|||
"_cw": {
|
||||
"hide": string;
|
||||
"show": string;
|
||||
"chars": string;
|
||||
"files": string;
|
||||
"chars": ParameterizedString<"count">;
|
||||
"files": ParameterizedString<"count">;
|
||||
};
|
||||
"_poll": {
|
||||
"noOnlyOneChoice": string;
|
||||
"choiceN": string;
|
||||
"choiceN": ParameterizedString<"n">;
|
||||
"noMore": string;
|
||||
"canMultipleVote": string;
|
||||
"expiration": string;
|
||||
|
@ -2232,16 +2239,16 @@ export interface Locale {
|
|||
"deadlineDate": string;
|
||||
"deadlineTime": string;
|
||||
"duration": string;
|
||||
"votesCount": string;
|
||||
"totalVotes": string;
|
||||
"votesCount": ParameterizedString<"n">;
|
||||
"totalVotes": ParameterizedString<"n">;
|
||||
"vote": string;
|
||||
"showResult": string;
|
||||
"voted": string;
|
||||
"closed": string;
|
||||
"remainingDays": string;
|
||||
"remainingHours": string;
|
||||
"remainingMinutes": string;
|
||||
"remainingSeconds": string;
|
||||
"remainingDays": ParameterizedString<"d" | "h">;
|
||||
"remainingHours": ParameterizedString<"h" | "m">;
|
||||
"remainingMinutes": ParameterizedString<"m" | "s">;
|
||||
"remainingSeconds": ParameterizedString<"s">;
|
||||
};
|
||||
"_visibility": {
|
||||
"public": string;
|
||||
|
@ -2281,7 +2288,7 @@ export interface Locale {
|
|||
"changeAvatar": string;
|
||||
"changeBanner": string;
|
||||
"verifiedLinkDescription": string;
|
||||
"avatarDecorationMax": string;
|
||||
"avatarDecorationMax": ParameterizedString<"max">;
|
||||
};
|
||||
"_exportOrImport": {
|
||||
"allNotes": string;
|
||||
|
@ -2404,16 +2411,16 @@ export interface Locale {
|
|||
};
|
||||
"_notification": {
|
||||
"fileUploaded": string;
|
||||
"youGotMention": string;
|
||||
"youGotReply": string;
|
||||
"youGotQuote": string;
|
||||
"youRenoted": string;
|
||||
"youGotMention": ParameterizedString<"name">;
|
||||
"youGotReply": ParameterizedString<"name">;
|
||||
"youGotQuote": ParameterizedString<"name">;
|
||||
"youRenoted": ParameterizedString<"name">;
|
||||
"youWereFollowed": string;
|
||||
"youReceivedFollowRequest": string;
|
||||
"yourFollowRequestAccepted": string;
|
||||
"pollEnded": string;
|
||||
"newNote": string;
|
||||
"unreadAntennaNote": string;
|
||||
"unreadAntennaNote": ParameterizedString<"name">;
|
||||
"roleAssigned": string;
|
||||
"emptyPushNotificationMessage": string;
|
||||
"achievementEarned": string;
|
||||
|
@ -2421,9 +2428,9 @@ export interface Locale {
|
|||
"checkNotificationBehavior": string;
|
||||
"sendTestNotification": string;
|
||||
"notificationWillBeDisplayedLikeThis": string;
|
||||
"reactedBySomeUsers": string;
|
||||
"renotedBySomeUsers": string;
|
||||
"followedBySomeUsers": string;
|
||||
"reactedBySomeUsers": ParameterizedString<"n">;
|
||||
"renotedBySomeUsers": ParameterizedString<"n">;
|
||||
"followedBySomeUsers": ParameterizedString<"n">;
|
||||
"_types": {
|
||||
"all": string;
|
||||
"note": string;
|
||||
|
@ -2480,8 +2487,8 @@ export interface Locale {
|
|||
};
|
||||
};
|
||||
"_dialog": {
|
||||
"charactersExceeded": string;
|
||||
"charactersBelow": string;
|
||||
"charactersExceeded": ParameterizedString<"current" | "max">;
|
||||
"charactersBelow": ParameterizedString<"current" | "min">;
|
||||
};
|
||||
"_disabledTimeline": {
|
||||
"title": string;
|
||||
|
|
|
@ -10,6 +10,7 @@ import { I18n } from '@/scripts/i18n.js';
|
|||
|
||||
export const i18n = markRaw(new I18n<Locale>(locale));
|
||||
|
||||
export function updateI18n(newLocale) {
|
||||
i18n.ts = newLocale;
|
||||
export function updateI18n(newLocale: Locale) {
|
||||
// @ts-expect-error -- private field
|
||||
i18n.locale = newLocale;
|
||||
}
|
||||
|
|
|
@ -2,33 +2,114 @@
|
|||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
|
||||
|
||||
export class I18n<T extends Record<string, any>> {
|
||||
public ts: T;
|
||||
type FlattenKeys<T extends ILocale, TPrediction> = keyof {
|
||||
[K in keyof T as T[K] extends ILocale
|
||||
? FlattenKeys<T[K], TPrediction> extends infer C extends string
|
||||
? `${K & string}.${C}`
|
||||
: never
|
||||
: T[K] extends TPrediction
|
||||
? K
|
||||
: never]: T[K];
|
||||
};
|
||||
|
||||
constructor(locale: T) {
|
||||
this.ts = locale;
|
||||
type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString<string>>> = T extends ILocale
|
||||
? TKey extends `${infer K}.${infer C}`
|
||||
// @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString<string>> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
|
||||
? ParametersOf<T[K], C>
|
||||
: TKey extends keyof T
|
||||
? T[TKey] extends ParameterizedString<infer P>
|
||||
? P
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Ts<T extends ILocale> = {
|
||||
readonly [K in keyof T as T[K] extends ParameterizedString<string> ? never : K]: T[K] extends ILocale ? Ts<T[K]> : string;
|
||||
};
|
||||
|
||||
export class I18n<T extends ILocale> {
|
||||
constructor(private locale: T) {
|
||||
//#region BIND
|
||||
this.t = this.t.bind(this);
|
||||
//#endregion
|
||||
}
|
||||
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, string | number>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
|
||||
public get ts(): Ts<T> {
|
||||
if (_DEV_) {
|
||||
class Handler<TTarget extends object> implements ProxyHandler<TTarget> {
|
||||
get(target: TTarget, p: string | symbol): unknown {
|
||||
const value = target[p as keyof TTarget];
|
||||
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replace(`{${k}}`, v.toString());
|
||||
if (typeof value === 'object') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 実際には null がくることはないので。
|
||||
return new Proxy(value!, new Handler<TTarget[keyof TTarget] & object>());
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const parameters = Array.from(value.matchAll(/\{(\w+)\}/g)).map(([, parameter]) => parameter);
|
||||
|
||||
if (parameters.length) {
|
||||
console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
console.error(`Unexpected locale key: ${String(p)}`);
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return str;
|
||||
} catch (err) {
|
||||
console.warn(`missing localization '${key}'`);
|
||||
return key;
|
||||
|
||||
return new Proxy(this.locale, new Handler()) as Ts<T>;
|
||||
}
|
||||
|
||||
return this.locale as Ts<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated なるべくこのメソッド使うよりも locale 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
|
||||
*/
|
||||
public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
|
||||
public t<TKey extends FlattenKeys<T, ParameterizedString<string>>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
|
||||
public t(key: string, args?: { readonly [_: string]: string | number }) {
|
||||
let str: string | ParameterizedString<string> | ILocale = this.locale;
|
||||
|
||||
for (const k of key.split('.')) {
|
||||
str = str[k];
|
||||
|
||||
if (_DEV_) {
|
||||
if (typeof str === 'undefined') {
|
||||
console.error(`Unexpected locale key: ${key}`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args) {
|
||||
if (_DEV_) {
|
||||
const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
|
||||
|
||||
if (missing.length) {
|
||||
console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
const search = `{${k}}`;
|
||||
|
||||
if (_DEV_) {
|
||||
if (!(str as string).includes(search)) {
|
||||
console.error(`Unexpected locale parameter: ${k} at ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
str = (str as string).replace(search, v.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue