summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--locales/generateDTS.js195
-rw-r--r--locales/index.d.ts229
-rw-r--r--packages/frontend/src/i18n.ts5
-rw-r--r--packages/frontend/src/scripts/i18n.ts113
4 files changed, 380 insertions, 162 deletions
diff --git a/locales/generateDTS.js b/locales/generateDTS.js
index d3afdd6e15..6eb5bd630d 100644
--- a/locales/generateDTS.js
+++ b/locales/generateDTS.js
@@ -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.createInterfaceDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier('Locale'),
- undefined,
- undefined,
- members,
- ),
ts.factory.createVariableStatement(
[ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)],
ts.factory.createVariableDeclarationList(
- [ts.factory.createVariableDeclaration(
- ts.factory.createIdentifier('locales'),
+ [
+ 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.createTypeLiteralNode([ts.factory.createIndexSignature(
+ ts.factory.createTypeReferenceNode(
+ ts.factory.createIdentifier('T'),
undefined,
- [ts.factory.createParameterDeclaration(
+ ),
+ ),
+ ],
+ ),
+ 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('lang'),
+ 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('Locale'),
+ ts.factory.createIdentifier('ParameterizedString'),
+ [ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)],
+ ),
+ ts.factory.createTypeReferenceNode(
+ ts.factory.createIdentifier('ILocale'),
undefined,
),
- )]),
- undefined,
- )],
- ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
+ ]),
+ ),
+ ],
+ ),
+ ts.factory.createInterfaceDeclaration(
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
+ ts.factory.createIdentifier('Locale'),
+ 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(
+ 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',
+ );
}
diff --git a/locales/index.d.ts b/locales/index.d.ts
index a659e790cc..a22cb63507 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -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;
diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts
index 858db74dac..c5c4ccf820 100644
--- a/packages/frontend/src/i18n.ts
+++ b/packages/frontend/src/i18n.ts
@@ -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;
}
diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts
index 8e5f17f38a..55b5371950 100644
--- a/packages/frontend/src/scripts/i18n.ts
+++ b/packages/frontend/src/scripts/i18n.ts
@@ -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 (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 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 (args) {
- for (const [k, v] of Object.entries(args)) {
- str = str.replace(`{${k}}`, v.toString());
+ if (_DEV_) {
+ if (typeof str === 'undefined') {
+ console.error(`Unexpected locale key: ${key}`);
+ return key;
}
}
- return str;
- } catch (err) {
- console.warn(`missing localization '${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;
}
}