Skip to content

Commit a038770

Browse files
committed
feat: add localization support for operator names + implementation
1 parent 8196dc4 commit a038770

File tree

14 files changed

+664
-109
lines changed

14 files changed

+664
-109
lines changed

scripts/shared.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fetch from 'node-fetch'
44
import { pinyin } from 'pinyin'
55
import simplebig from 'simplebig'
66

7-
type Profession = { id: string; name: string }
7+
type Profession = { id: string; name: string; name_en?: string }
88
type Professions = (Profession & { sub: Profession[] })[]
99

1010
export async function fileExists(file: string) {
@@ -48,11 +48,14 @@ function transformOperatorName(name: string) {
4848
}
4949
}
5050

51-
const CHARACTER_TABLE_JSON_URL =
51+
const CHARACTER_TABLE_JSON_URL_CN =
5252
'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/character_table.json'
53-
const UNIEQUIP_TABLE_JSON_URL =
53+
const UNIEQUIP_TABLE_JSON_URL_CN =
5454
'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/uniequip_table.json'
55-
55+
const CHARACTER_TABLE_JSON_URL_EN =
56+
'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData_YoStar/main/en_US/gamedata/excel/character_table.json'
57+
const UNIEQUIP_TABLE_JSON_URL_EN =
58+
'https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData_YoStar/main/en_US/gamedata/excel/uniequip_table.json'
5659
const CHARACTER_BLOCKLIST = [
5760
'char_512_aprot', // 暮落(集成战略):It's just not gonna be there.
5861
'token_10012_rosmon_shield', // 迷迭香的战术装备:It's just not gonna be there.
@@ -74,37 +77,55 @@ async function json(url: string) {
7477
}
7578

7679
export async function getOperators() {
77-
const [charTable, uniequipTable] = await Promise.all([
78-
json(CHARACTER_TABLE_JSON_URL),
79-
json(UNIEQUIP_TABLE_JSON_URL),
80-
])
80+
const [charTableCN, uniequipTableCN, charTableEN, uniequipTableEN] =
81+
await Promise.all([
82+
json(CHARACTER_TABLE_JSON_URL_CN),
83+
json(UNIEQUIP_TABLE_JSON_URL_CN),
84+
json(CHARACTER_TABLE_JSON_URL_EN),
85+
json(UNIEQUIP_TABLE_JSON_URL_EN),
86+
])
8187

82-
const { subProfDict } = uniequipTable
88+
const { subProfDict: subProfDictCN } = uniequipTableCN
89+
const { subProfDict: subProfDictEN } = uniequipTableEN
8390

84-
const opIds = Object.keys(charTable)
91+
const opIds = Object.keys(charTableCN)
8592
const professions: Professions = []
8693
const result = uniqBy(
8794
opIds.flatMap((id) => {
88-
const op = charTable[id]
95+
const op = charTableCN[id]
96+
const enName = charTableEN[id]?.name || op.appellation || op.name
97+
8998
if (['TRAP'].includes(op.profession)) return []
9099

91100
if (!['TOKEN'].includes(op.profession)) {
92101
const prof = professions.find((p) => p.id === op.profession)
93102
if (!prof) {
103+
const enSubProfName =
104+
subProfDictEN?.[op.subProfessionId]?.subProfessionName ||
105+
subProfDictCN[op.subProfessionId].subProfessionName
106+
94107
professions.push({
95108
id: op.profession,
96109
name: PROFESSION_NAMES[op.profession],
110+
name_en:
111+
op.profession.charAt(0) + op.profession.slice(1).toLowerCase(),
97112
sub: [
98113
{
99114
id: op.subProfessionId,
100-
name: subProfDict[op.subProfessionId].subProfessionName,
115+
name: subProfDictCN[op.subProfessionId].subProfessionName,
116+
name_en: enSubProfName,
101117
},
102118
],
103119
})
104120
} else if (!prof.sub.find((p) => p.id === op.subProfessionId)) {
121+
const enSubProfName =
122+
subProfDictEN?.[op.subProfessionId]?.subProfessionName ||
123+
subProfDictCN[op.subProfessionId].subProfessionName
124+
105125
prof.sub.push({
106126
id: op.subProfessionId,
107-
name: subProfDict[op.subProfessionId].subProfessionName,
127+
name: subProfDictCN[op.subProfessionId].subProfessionName,
128+
name_en: enSubProfName,
108129
})
109130
}
110131
}
@@ -113,6 +134,7 @@ export async function getOperators() {
113134
id: id,
114135
prof: op.profession,
115136
subProf: op.subProfessionId,
137+
name_en: enName,
116138
...transformOperatorName(op.name),
117139
rarity:
118140
op.subProfessionId === 'notchar1'

src/components/OperationCard.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { OpDifficulty, Operation } from 'models/operation'
1313
import { useLevels } from '../apis/level'
1414
import { useTranslation } from '../i18n/i18n'
1515
import { createCustomLevel, findLevelByStageName } from '../models/level'
16+
import { useLocalizedOperatorName } from '../models/operator'
1617
import { Paragraphs } from './Paragraphs'
1718
import { ReLinkDiv } from './ReLinkDiv'
1819
import { UserName } from './UserName'
@@ -251,7 +252,7 @@ const OperatorTags = ({ operation }: { operation: Operation }) => {
251252
<div>
252253
{opers?.map(({ name, skill }, index) => (
253254
<Tag key={index} className="mr-2 last:mr-0 mb-1 last:mb-0">
254-
{`${name} ${skill ?? 1}`}
255+
{`${useLocalizedOperatorName(name)} ${skill ?? 1}`}
255256
</Tag>
256257
))}
257258
{groups?.map(({ name, opers }, index) => (
@@ -261,7 +262,10 @@ const OperatorTags = ({ operation }: { operation: Operation }) => {
261262
placement="top"
262263
content={
263264
opers
264-
?.map(({ name, skill }) => `${name} ${skill ?? 1}`)
265+
?.map(
266+
({ name, skill }) =>
267+
`${useLocalizedOperatorName(name)} ${skill ?? 1}`,
268+
)
265269
.join(', ') || t.components.OperationCard.no_operators
266270
}
267271
>

src/components/OperatorFilter.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import {
1010

1111
import clsx from 'clsx'
1212
import { getDefaultStore, useAtom } from 'jotai'
13+
import { useAtomValue } from 'jotai'
1314
import { compact } from 'lodash-es'
1415
import { FC, useEffect, useMemo, useState } from 'react'
1516

16-
import { useTranslation } from '../i18n/i18n'
17+
import { languageAtom, useTranslation } from '../i18n/i18n'
1718
import { OPERATORS } from '../models/operator'
1819
import {
1920
DEFAULT_OPERATOR_FILTER,
@@ -53,6 +54,7 @@ export const OperatorFilter: FC<OperatorFilterProps> = ({
5354
onChange,
5455
}) => {
5556
const t = useTranslation()
57+
const language = useAtomValue(languageAtom)
5658
const [savedFilter, setSavedFilter] = useAtom(operatorFilterAtom)
5759
const [dialogOpen, setDialogOpen] = useState(false)
5860
const [editingFilter, setEditingFilter] = useState<typeof savedFilter>(filter)
@@ -137,27 +139,29 @@ export const OperatorFilter: FC<OperatorFilterProps> = ({
137139
!filter.enabled && 'opacity-30',
138140
)}
139141
>
140-
{includedOperators.map(({ id, name, rarity }) => (
142+
{includedOperators.map(({ id, name, name_en, rarity }) => (
141143
<Tag minimal key={id} className="py-0 pl-0" intent="primary">
142144
<div className="flex items-center gap-1 text-sm">
143145
<OperatorAvatar
144146
className="w-8 h-8"
145147
id={id}
146148
rarity={rarity}
147149
/>
148-
&nbsp;{name}&nbsp;
150+
&nbsp;{language === 'en' ? name_en : name}
151+
&nbsp;
149152
</div>
150153
</Tag>
151154
))}
152-
{excludedOperators.map(({ id, name, rarity }) => (
155+
{excludedOperators.map(({ id, name, name_en, rarity }) => (
153156
<Tag minimal key={id} className="py-0 pl-0" intent="danger">
154157
<div className="flex items-center gap-1 text-sm line-through">
155158
<OperatorAvatar
156159
className="w-8 h-8"
157160
id={id}
158161
rarity={rarity}
159162
/>
160-
&nbsp;{name}&nbsp; {/* 两边加空格让删除线更显眼一些 */}
163+
&nbsp;{language === 'en' ? name_en : name}
164+
&nbsp; {/* 两边加空格让删除线更显眼一些 */}
161165
</div>
162166
</Tag>
163167
))}

src/components/OperatorSelect.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { MultiSelect2 } from '@blueprintjs/select'
44

55
import clsx from 'clsx'
66
import Fuse from 'fuse.js'
7+
import { useAtomValue } from 'jotai'
78
import { compact } from 'lodash-es'
89
import { FC, useMemo } from 'react'
910

10-
import { useTranslation } from '../i18n/i18n'
11+
import { languageAtom, useTranslation } from '../i18n/i18n'
1112
import { OPERATORS } from '../models/operator'
1213
import { useDebouncedQuery } from '../utils/useDebouncedQuery'
1314
import { OperatorAvatar } from './editor/operator/EditorOperator'
@@ -26,13 +27,14 @@ export const OperatorSelect: FC<OperatorSelectProps> = ({
2627
onChange,
2728
}) => {
2829
const t = useTranslation()
30+
const language = useAtomValue(languageAtom)
2931
const { query, trimmedDebouncedQuery, updateQuery, onOptionMouseDown } =
3032
useDebouncedQuery()
3133

3234
const fuse = useMemo(
3335
() =>
3436
new Fuse(OPERATORS, {
35-
keys: ['name', 'alias', 'alt_name'],
37+
keys: ['name', 'name_en', 'alias', 'alt_name'],
3638
threshold: 0.3,
3739
}),
3840
[],
@@ -82,7 +84,7 @@ export const OperatorSelect: FC<OperatorSelectProps> = ({
8284
id={item.id}
8385
rarity={item.rarity}
8486
/>
85-
{item.name}
87+
{language === 'en' ? item.name_en : item.name}
8688
</div>
8789
}
8890
onClick={handleClick}
@@ -129,7 +131,7 @@ export const OperatorSelect: FC<OperatorSelectProps> = ({
129131
id={item.id}
130132
rarity={item.rarity}
131133
/>
132-
{item.name}
134+
{language === 'en' ? item.name_en : item.name}
133135
</div>
134136
)}
135137
popoverProps={{

src/components/editor/operator/EditorOperator.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { Icon, IconSize, MenuItem } from '@blueprintjs/core'
22

33
import clsx from 'clsx'
44
import Fuse from 'fuse.js'
5+
import { useAtomValue } from 'jotai'
56
import { useMemo } from 'react'
67
import { FieldValues, useController } from 'react-hook-form'
78

89
import { EditorFieldProps } from 'components/editor/EditorFieldProps'
910

10-
import { useTranslation } from '../../../i18n/i18n'
11+
import { languageAtom, useTranslation } from '../../../i18n/i18n'
1112
import { CopilotDocV1 } from '../../../models/copilot.schema'
1213
import { OPERATORS } from '../../../models/operator'
1314
import { Suggest } from '../../Suggest'
@@ -27,6 +28,7 @@ const createArbitraryOperator = (name: string): OperatorInfo => ({
2728
alias: '',
2829
alt_name: '',
2930
subProf: '',
31+
name_en: '',
3032
prof: '',
3133
rarity: 0,
3234
})
@@ -43,6 +45,7 @@ export const EditorOperatorName = <T extends FieldValues>({
4345
operators?: CopilotDocV1.Operator[]
4446
}) => {
4547
const t = useTranslation()
48+
const language = useAtomValue(languageAtom)
4649

4750
const entityName = useMemo(
4851
() =>
@@ -90,7 +93,7 @@ export const EditorOperatorName = <T extends FieldValues>({
9093
const fuse = useMemo(
9194
() =>
9295
new Fuse(items, {
93-
keys: ['name', 'alias', 'alt_name'],
96+
keys: ['name', 'name_en', 'alias', 'alt_name'],
9497
threshold: 0.3,
9598
}),
9699
[items],
@@ -107,7 +110,11 @@ export const EditorOperatorName = <T extends FieldValues>({
107110
itemRenderer={(item, { handleClick, handleFocus, modifiers }) => (
108111
<MenuItem
109112
key={'id' in item ? item.id : item.name}
110-
text={item.name}
113+
text={
114+
isOperator(item) && language === 'en' && item.name_en
115+
? item.name_en
116+
: item.name
117+
}
111118
icon={
112119
isOperator(item) ? (
113120
<OperatorAvatar id={item.id} size="small" />
@@ -123,7 +130,11 @@ export const EditorOperatorName = <T extends FieldValues>({
123130
)}
124131
onItemSelect={(item) => onChange(item.name)}
125132
selectedItem={createArbitraryOperator((value || '') as string)}
126-
inputValueRenderer={(item) => item.name}
133+
inputValueRenderer={(item) =>
134+
isOperator(item) && language === 'en' && item.name_en
135+
? item.name_en
136+
: item.name
137+
}
127138
createNewItemFromQuery={(query) => createArbitraryOperator(query)}
128139
createNewItemRenderer={(query, active, handleClick) => (
129140
<MenuItem

src/components/editor/operator/EditorOperatorItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import clsx from 'clsx'
55
import type { CopilotDocV1 } from 'models/copilot.schema'
66

77
import { useTranslation } from '../../../i18n/i18n'
8-
import { OPERATORS, getSkillUsageTitle } from '../../../models/operator'
8+
import { OPERATORS, getSkillUsageTitle, useLocalizedOperatorName } from '../../../models/operator'
99
import { SortableItemProps } from '../../dnd'
1010
import { CardDeleteOption, CardEditOption } from '../CardOptions'
1111
import { OperatorAvatar } from './EditorOperator'
@@ -52,7 +52,7 @@ export const EditorOperatorItem = ({
5252
/>
5353
<OperatorAvatar id={id} size="large" />
5454
<div className="ml-4 flex-grow">
55-
<h3 className="font-bold leading-none mb-1">{operator.name}</h3>
55+
<h3 className="font-bold leading-none mb-1">{useLocalizedOperatorName(operator.name)}</h3>
5656
<div className="text-gray-400 text-xs">
5757
{t.components.editor.operator.EditorOperatorItem.skill_number({
5858
count: operator.skill,

src/components/editor/operator/sheet/SheetGroup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAtomValue } from 'jotai'
44
import { FC, useMemo, useState } from 'react'
55

66
import { AppToaster } from 'components/Toaster'
7-
import { OPERATORS, PROFESSIONS } from 'models/operator'
7+
import { OPERATORS, PROFESSIONS, useLocalizedOperatorName } from 'models/operator'
88
import { favGroupAtom } from 'store/useFavGroups'
99

1010
import { useTranslation } from '../../../../i18n/i18n'

src/components/editor/operator/sheet/sheetGroup/OperatorInGroupItem.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FC } from 'react'
55
import { OperatorAvatar } from '../../EditorOperator'
66
import { Operator } from '../../EditorSheet'
77
import { SkillAboutTrigger } from '../SheetOperatorSkillAbout'
8+
import { useLocalizedOperatorName } from 'models/operator'
89

910
export interface OperatorInGroupItemProp {
1011
operatorInfo: Operator
@@ -20,7 +21,7 @@ export const OperatorInGroupItem: FC<OperatorInGroupItemProp> = ({
2021
<div className="flex items-center">
2122
<OperatorAvatar name={name} size="large" />
2223
<p className="font-bold leading-none text-center truncate ml-2 mr-auto">
23-
{name}
24+
{useLocalizedOperatorName(name)}
2425
</p>
2526
</div>
2627
{!!onOperatorSkillChange && (

src/components/editor/operator/sheet/sheetGroup/SheetOperatorEditor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { OperatorNoData } from '../SheetNoneData'
3232
import { useSheet } from '../SheetProvider'
3333
import { CollapseButton } from './CollapseButton'
34+
import { useLocalizedOperatorName } from 'models/operator'
3435

3536
export interface SheetOperatorEditorProp extends SheetOperatorEditorFormProp {}
3637

@@ -333,7 +334,7 @@ const OperatorSelectorItem: FC<{
333334
>
334335
<OperatorAvatar name={name} size="large" />
335336
<p className="font-bold leading-none text-center mt-3 truncate">
336-
{name}
337+
{useLocalizedOperatorName(name)}
337338
</p>
338339
</Card>
339340
)

src/components/editor/operator/sheet/sheetOperator/SheetOperatorFilterProvider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ const generateCustomizedOperInfo = (name: string): OperatorInfo => ({
111111
name,
112112
prof: 'TOKEN',
113113
subProf: 'customized',
114+
name_en: '',
114115
alias: 'customized-operator',
115116
rarity: 0,
116117
alt_name: 'custormized operator named' + name,

0 commit comments

Comments
 (0)