Complete Cocos Creator port with level bundles, themes, and tooling.

Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:30:58 +08:00
parent cba5105908
commit d393302388
6248 changed files with 17322729 additions and 11036 deletions

View File

@@ -0,0 +1,173 @@
'use strict';
/** AppRoot InspectorSwitchLevel + 游戏调试(对齐 Unity TestPlayer */
function compUuid(dump) {
if (!dump || !dump.value) return '';
const u = dump.value.uuid;
return (u && u.value) ? u.value : (typeof u === 'string' ? u : '');
}
function nodeUuid(dump) {
if (!dump || !dump.value) return '';
const n = dump.value.node;
return (n && n.value && n.value.uuid) ? n.value.uuid : '';
}
function getMessageProtocolScene(el) {
let element = el;
while (element) {
const root = element.getRootNode && element.getRootNode();
element = element.parentElement || (root && root.host) || null;
if (element && element.messageProtocol && element.messageProtocol.scene) {
return element.messageProtocol.scene;
}
}
return 'scene';
}
async function callMethod(contextEl, uuid, name, args) {
if (!uuid || typeof Editor === 'undefined') return;
const protocols = [];
const primary = getMessageProtocolScene(contextEl);
if (primary) protocols.push(primary);
if (protocols.indexOf('scene') < 0) protocols.push('scene');
let lastErr;
for (let i = 0; i < protocols.length; i++) {
try {
await Editor.Message.request(protocols[i], 'execute-component-method', {
uuid,
name,
args: args || [],
});
return;
} catch (e) {
lastErr = e;
}
}
console.warn('[game-controller-inspector]', name, lastErr);
}
async function findGameControllerUuid(nu) {
if (!nu) return null;
try {
const node = await Editor.Message.request('scene', 'query-node', nu);
for (const c of node?.__comps__ || []) {
if (c.type === 'GameController' || c.type === '8940aPSKJhGKKIBE/qKnFm7') {
return c.value.uuid.value;
}
}
} catch (e) {
console.warn('[game-controller-inspector] query-node failed', e);
}
return null;
}
async function callGame(contextEl, dump, method, args) {
const gc = await findGameControllerUuid(nodeUuid(dump));
if (!gc) {
console.warn('[game-controller-inspector] 请先点预览 ▶,等关卡加载后再调试');
return;
}
await callMethod(contextEl, gc, method, args);
}
function parseStep(raw) {
const n = parseInt(String(raw || '1'), 10);
return Number.isNaN(n) || n === 0 ? 1 : n;
}
let lastDebugAt = 0;
function guardDebug(fn) {
return () => {
const now = Date.now();
if (now - lastDebugAt < 350) return;
lastDebugAt = now;
fn();
};
}
module.exports = Editor.Panel.define({
template: `
<div class="gc-footer">
<ui-label class="hint">① 预览 ▶ ② GameController 填 inputLevel ③ 点 SwitchLevel</ui-label>
<div class="row level-nav">
<ui-button class="btn-prev">上一关</ui-button>
<ui-button class="btn-next">下一关</ui-button>
</div>
<ui-button class="btn-switch blue">SwitchLevel</ui-button>
<ui-label class="sep">— 游戏调试(须预览运行中)—</ui-label>
<div class="row">
<ui-label class="lbl">步数</ui-label>
<ui-input class="step" value="1"></ui-input>
</div>
<div class="row">
<ui-button class="fwd">前进</ui-button>
<ui-button class="back">后退</ui-button>
<ui-button class="jump">跳</ui-button>
</div>
<div class="row">
<ui-button class="rot-l">左转</ui-button>
<ui-button class="rot-r">右转</ui-button>
<ui-button class="info">坐标</ui-button>
</div>
<div class="row">
<ui-button class="end">结束输入</ui-button>
<ui-button class="veh">载具+1</ui-button>
<ui-button class="reset">重置关卡</ui-button>
</div>
</div>
`,
style: `
.gc-footer { margin-top: 6px; }
.gc-footer .hint, .gc-footer .sep {
opacity: 0.8;
font-size: 11px;
margin: 6px 0;
display: block;
}
.gc-footer .row { display: flex; gap: 4px; margin-bottom: 6px; }
.gc-footer .lbl { font-size: 11px; align-self: center; min-width: 28px; }
.gc-footer .step { flex: 1; }
.gc-footer ui-button { flex: 1; }
.gc-footer .level-nav { margin-bottom: 4px; }
.gc-footer .btn-switch { width: 100%; margin-bottom: 4px; }
`,
$: {
hint: '.hint',
step: '.step',
btnSwitch: '.btn-switch',
btnPrev: '.btn-prev',
btnNext: '.btn-next',
fwd: '.fwd',
back: '.back',
jump: '.jump',
rotL: '.rot-l',
rotR: '.rot-r',
info: '.info',
end: '.end',
veh: '.veh',
reset: '.reset',
},
ready() {
const ctx = () => this.$this || this.$.btnSwitch;
const step = () => parseStep(this.$.step.value);
this.$.btnPrev.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'prevLevel', [])));
this.$.btnNext.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'nextLevel', [])));
this.$.btnSwitch.addEventListener('confirm', () => {
void callMethod(ctx(), compUuid(this.dump), 'switchLevelFromBootstrap', []);
});
this.$.fwd.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugMove', [step()])));
this.$.back.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugMove', [-step()])));
this.$.jump.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugJump', [])));
this.$.rotL.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugRotateLeft', [1])));
this.$.rotR.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugRotateRight', [1])));
this.$.info.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugPlayerInfo', [])));
this.$.end.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugInputEnd', [])));
this.$.veh.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugVehicleMove', [1])));
this.$.reset.addEventListener('confirm', guardDebug(() => void callGame(ctx(), this.dump, 'debugResetLevel', [])));
},
update(dump) {
if (dump) this.dump = dump;
},
});

View File

@@ -0,0 +1,124 @@
'use strict';
function compUuid(dump) {
if (!dump || !dump.value) return '';
const u = dump.value.uuid;
return (u && u.value) ? u.value : (typeof u === 'string' ? u : '');
}
function getMessageProtocolScene(el) {
let element = el;
while (element) {
const root = element.getRootNode && element.getRootNode();
element = element.parentElement || (root && root.host) || null;
if (element && element.messageProtocol && element.messageProtocol.scene) {
return element.messageProtocol.scene;
}
}
return 'scene';
}
async function callMethod(contextEl, uuid, name, args) {
if (!uuid || typeof Editor === 'undefined') return;
const protocols = [];
const primary = getMessageProtocolScene(contextEl);
if (primary) protocols.push(primary);
if (protocols.indexOf('scene') < 0) protocols.push('scene');
let lastErr;
for (let i = 0; i < protocols.length; i++) {
try {
await Editor.Message.request(protocols[i], 'execute-component-method', {
uuid,
name,
args: args || [],
});
return;
} catch (e) {
lastErr = e;
}
}
console.warn('[game-controller-inspector]', name, lastErr);
}
function parseStep(raw) {
const n = parseInt(String(raw || '1'), 10);
return Number.isNaN(n) || n === 0 ? 1 : n;
}
module.exports = Editor.Panel.define({
template: `
<div class="gc-debug">
<ui-label class="hint">预览 ▶ 运行后可用(对齐 Unity TestPlayer</ui-label>
<div class="row">
<ui-label class="lbl">步数</ui-label>
<ui-input class="step" value="1"></ui-input>
</div>
<div class="row">
<ui-button class="fwd">前进</ui-button>
<ui-button class="back">后退</ui-button>
<ui-button class="jump">跳</ui-button>
</div>
<div class="row">
<ui-button class="rot-l">左转</ui-button>
<ui-button class="rot-r">右转</ui-button>
<ui-button class="info">坐标</ui-button>
</div>
<div class="row">
<ui-button class="end">结束输入</ui-button>
<ui-button class="veh">载具+1</ui-button>
<ui-button class="reset">重置关卡</ui-button>
</div>
<div class="row level-nav">
<ui-button class="btn-prev">上一关</ui-button>
<ui-button class="btn-next">下一关</ui-button>
</div>
<ui-button class="btn-switch blue">SwitchLevel</ui-button>
</div>
`,
style: `
.gc-debug { margin-top: 6px; }
.gc-debug .hint { opacity: 0.75; font-size: 11px; margin-bottom: 8px; display: block; }
.gc-debug .row { display: flex; gap: 4px; margin-bottom: 6px; }
.gc-debug .lbl { font-size: 11px; align-self: center; }
.gc-debug .step { flex: 1; }
.gc-debug ui-button { flex: 1; }
.gc-debug .level-nav { margin-top: 2px; }
.gc-debug .btn-switch { width: 100%; margin-top: 4px; }
`,
$: {
hint: '.hint',
step: '.step',
fwd: '.fwd',
back: '.back',
jump: '.jump',
rotL: '.rot-l',
rotR: '.rot-r',
info: '.info',
end: '.end',
veh: '.veh',
reset: '.reset',
btnSwitch: '.btn-switch',
btnPrev: '.btn-prev',
btnNext: '.btn-next',
},
ready() {
const uuid = () => compUuid(this.dump);
const step = () => parseStep(this.$.step.value);
const ctx = () => this.$this || this.$.btnSwitch;
this.$.fwd.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugMove', [step()]));
this.$.back.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugMove', [-step()]));
this.$.jump.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugJump', []));
this.$.rotL.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugRotateLeft', [1]));
this.$.rotR.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugRotateRight', [1]));
this.$.info.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugPlayerInfo', []));
this.$.end.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugInputEnd', []));
this.$.veh.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugVehicleMove', [1]));
this.$.reset.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'debugResetLevel', []));
this.$.btnPrev.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'prevLevel', []));
this.$.btnNext.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'nextLevel', []));
this.$.btnSwitch.addEventListener('confirm', () => void callMethod(ctx(), uuid(), 'clickSwitchLevel', []));
},
update(dump) {
if (dump) this.dump = dump;
},
});

View File

@@ -0,0 +1,105 @@
'use strict';
const fs = require('fs');
const path = require('path');
async function findComp(nodeUuid, typeNames) {
if (!nodeUuid || typeof Editor === 'undefined') return null;
const node = await Editor.Message.request('scene', 'query-node', nodeUuid);
const comps = node?.__comps__ || [];
for (const c of comps) {
if (typeNames.indexOf(c.type) >= 0) {
return c.value.uuid.value;
}
}
return null;
}
async function callOnNode(nodeUuid, typeNames, method, args) {
try {
const uuid = await findComp(nodeUuid, typeNames);
if (!uuid) {
console.warn('[game-controller-inspector] 选中节点上未找到', typeNames.join('/'));
return;
}
await Editor.Message.request('scene', 'execute-component-method', {
uuid,
name: method,
args: args || [],
});
} catch (e) {
console.warn('[game-controller-inspector]', method, e);
}
}
const BOOTSTRAP_TYPES = ['AppBootstrap', 'c0468XFPX1InLN14gtZ5PLf'];
const GAME_TYPES = ['GameController'];
function readLevelDatabaseJson() {
if (typeof Editor === 'undefined' || !Editor.Project?.path) return null;
const dbPath = path.join(Editor.Project.path, 'assets', 'level-data', 'levels-database.json');
if (!fs.existsSync(dbPath)) {
console.warn('[game-controller-inspector] 未找到 assets/level-data/levels-database.json');
return null;
}
try {
return JSON.parse(fs.readFileSync(dbPath, 'utf8'));
} catch (e) {
console.warn('[game-controller-inspector] 解析关卡库失败', e);
return null;
}
}
/** 编辑器 ▶ 预览:注入关卡库(不再打包进 resources bundle */
function installPreviewLevelDbHook() {
if (typeof Editor === 'undefined' || !Editor.Message?.addBroadcastListener) return;
Editor.Message.addBroadcastListener('scene:ready-for-preview', () => {
const json = readLevelDatabaseJson();
if (!json) return;
Editor.Message.request('scene', 'execute-scene-script', {
name: 'game-controller-inspector',
method: 'apply',
args: [json],
}).catch((e) => {
console.warn('[game-controller-inspector] 预览注入关卡库失败,请通过 scratch-gui 嵌入测试', e);
});
});
}
exports.load = function load() {
console.log('[game-controller-inspector] v3.4 — AppBootstrap 面板含游戏调试');
installPreviewLevelDbHook();
};
exports.unload = function unload() {
console.log('[game-controller-inspector] 已卸载');
};
async function callGameOnSelection(method, args) {
const uuids = Editor.Selection.getSelected('node');
if (!uuids || !uuids.length) {
console.warn('[game-controller-inspector] 请先在层级管理器选中含 GameController 的节点');
return;
}
await callOnNode(uuids[0], GAME_TYPES, method, args);
}
exports.methods = {
async switchLevelOnSelection() {
const uuids = Editor.Selection.getSelected('node');
if (!uuids || !uuids.length) {
console.warn('[game-controller-inspector] 请先在层级管理器选中 AppRoot');
return;
}
await callOnNode(uuids[0], BOOTSTRAP_TYPES, 'switchLevelFromBootstrap', []);
},
async debugMoveOnSelection() {
await callGameOnSelection('debugMove', [1]);
},
async debugJumpOnSelection() {
await callGameOnSelection('debugJump', []);
},
async debugPlayerInfoOnSelection() {
await callGameOnSelection('debugPlayerInfo', []);
},
};

View File

@@ -0,0 +1,10 @@
'use strict';
/** 编辑器 ▶ 预览:注入 data/levels-database.json不打包进 resources */
exports.methods = {
apply(json) {
if (typeof window !== 'undefined' && json && typeof json === 'object') {
window.__tfrhLevelsDatabaseJson = json;
}
},
};

View File

@@ -1,16 +1,58 @@
{
"package_version": 2,
"name": "game-controller-inspector",
"version": "1.0.0",
"description": "GameController Inspector 调试按钮(对齐 Unity TestGame2",
"version": "3.3.0",
"description": "GameController 调试按钮 + AppBootstrap SwitchLevel",
"author": "tfrh",
"editor": ">=3.8.0",
"main": "./dist/main.js",
"scene-script": "./dist/scene-script.js",
"contributions": {
"inspector": {
"section": {
"footer": {
"node": {
"GameController": "./dist/inspector.js"
"AppBootstrap": "./dist/footer.js",
"c0468XFPX1InLN14gtZ5PLf": "./dist/footer.js",
"GameController": "./dist/game-controller-footer.js",
"8940aPSKJhGKKIBE/qKnFm7": "./dist/game-controller-footer.js"
}
}
},
"menu": [
{
"path": "扩展",
"label": "SwitchLevel当前选中 AppRoot",
"message": "switch-level"
},
{
"path": "扩展/游戏调试",
"label": "Player 前进 1 格",
"message": "debug-move-fwd"
},
{
"path": "扩展/游戏调试",
"label": "Player 跳跃",
"message": "debug-jump"
},
{
"path": "扩展/游戏调试",
"label": "Player 获取坐标",
"message": "debug-player-info"
}
],
"messages": {
"switch-level": {
"methods": ["switchLevelOnSelection"]
},
"debug-move-fwd": {
"methods": ["debugMoveOnSelection"]
},
"debug-jump": {
"methods": ["debugJumpOnSelection"]
},
"debug-player-info": {
"methods": ["debugPlayerInfoOnSelection"]
}
}
}
}

View File

@@ -1,83 +1,35 @@
/**
* GameController 自定义 Inspector(需启用扩展 game-controller-inspector
* 构建: 在扩展目录执行 npm run build或在 Creator 扩展管理器中启用
* GameController Inspector Footer — 关卡切换 + SwitchLevel
*/
export const template = `
<div class="game-controller-inspector">
<ui-prop type="dump" prop="dump"></ui-prop>
<ui-section header="关卡调试">
<ui-input id="input-level" placeholder="inputLevel"></ui-input>
<ui-button id="btn-switch">SwitchLevel</ui-button>
<ui-button id="btn-end">EndInput</ui-button>
</ui-section>
<ui-section header="主题">
<ui-input id="input-style" placeholder="inputStyle"></ui-input>
<ui-button id="btn-style">ChangeStyle</ui-button>
</ui-section>
<ui-section header="多人">
<ui-input id="input-coins" placeholder='coinStr JSON'></ui-input>
<ui-button id="btn-mult">StartMultPlay</ui-button>
</ui-section>
<ui-section header="音频">
<ui-button id="btn-mute">Mute</ui-button>
<ui-button id="btn-unmute">Unmute</ui-button>
</ui-section>
<div class="gc-footer">
<ui-label class="hint">预览 ▶ 后可用inputLevel 填 ID 或点上一关/下一关</ui-label>
<div class="row level-nav">
<ui-button class="btn-prev">上一关</ui-button>
<ui-button class="btn-next">下一关</ui-button>
</div>
<ui-button class="btn-switch">SwitchLevel</ui-button>
</div>
`;
export const style = `
.gc-footer { margin-top: 6px; }
.gc-footer .hint { opacity: 0.75; font-size: 11px; margin-bottom: 6px; display: block; }
.gc-footer .row { display: flex; gap: 4px; margin-bottom: 6px; }
.gc-footer .row ui-button { flex: 1; }
.gc-footer .btn-switch { width: 100%; }
`;
export const $ = {
inputLevel: '#input-level',
inputStyle: '#input-style',
inputCoins: '#input-coins',
btnSwitch: '#btn-switch',
btnEnd: '#btn-end',
btnStyle: '#btn-style',
btnMult: '#btn-mult',
btnMute: '#btn-mute',
btnUnmute: '#btn-unmute',
} as Record<string, string>;
hint: '.hint',
btnPrev: '.btn-prev',
btnNext: '.btn-next',
btnSwitch: '.btn-switch',
};
type Dump = { value: Record<string, { value: unknown }> };
export function update(this: { dump: Dump; $: Record<string, HTMLElement> }, dump: Dump) {
this.dump = dump;
const v = dump.value;
if (this.$.inputLevel && v.inputLevel) {
(this.$.inputLevel as unknown as { value: string }).value = String(v.inputLevel.value ?? '1');
}
if (this.$.inputStyle && v.inputStyle) {
(this.$.inputStyle as unknown as { value: string }).value = String(v.inputStyle.value ?? 'default');
}
if (this.$.inputCoins && v.coinStr) {
(this.$.inputCoins as unknown as { value: string }).value = String(v.coinStr.value ?? '[]');
}
export function update(dump: unknown) {
void dump;
}
function callMethod(uuid: string, name: string, args: unknown[] = []) {
// @ts-expect-error Editor global
if (typeof Editor !== 'undefined' && Editor.Message) {
// @ts-expect-error Editor API
Editor.Message.request('scene', 'execute-component-method', { uuid, name, args });
}
}
export function ready(this: { dump: Dump; $: Record<string, HTMLElement> }) {
const uuid = () => this.dump.value.uuid?.value as string;
this.$.btnSwitch?.addEventListener('confirm', () => {
const id = parseInt((this.$.inputLevel as unknown as { value: string }).value || '1', 10);
callMethod(uuid(), 'switchLevel', [id]);
});
this.$.btnEnd?.addEventListener('confirm', () => callMethod(uuid(), 'callSetIsInputEnd', [1]));
this.$.btnStyle?.addEventListener('confirm', () => {
const s = (this.$.inputStyle as unknown as { value: string }).value || 'default';
callMethod(uuid(), 'changeUIStyle', [s]);
});
this.$.btnMult?.addEventListener('confirm', () => {
const coins = (this.$.inputCoins as unknown as { value: string }).value || '[]';
callMethod(uuid(), 'startMultPlay', ['PlayerA1', coins]);
});
this.$.btnMute?.addEventListener('confirm', () => callMethod(uuid(), 'callMute', []));
this.$.btnUnmute?.addEventListener('confirm', () => callMethod(uuid(), 'callUnmute', []));
}
export function ready() {}

View File

@@ -0,0 +1,63 @@
# 关卡地图编辑level-map-editor
对齐 Unity **平铺调色板Tile Palette**:中间为关卡等距网格,右侧为可绘制的主题瓦片。
## 打开方式
1. 扩展管理器中启用本扩展,点击 **刷新**(版本应为 1.0.3
2. 顶部菜单任选其一:
- **面板 → level-map-editor → 关卡地图编辑**(推荐)
- **扩展 → 关卡地图编辑**
3. 控制台应出现 `[level-map-editor] extension loaded``Panel.open`
4. 若仍无窗口:关闭扩展 → 再开启 → 或重启 Creator
## 使用步骤
1. **调色板** 下拉选择主题(如 `sanxing` 三星堆)
2.**新建**(自动分配下一个关卡 ID`91601` 起,与 Unity / 主站 `BEGINNING_REAL_LVID=91601` 对齐)
- 或输入已有 **关卡 ID****加载**
3. 右侧点击瓦片WallBlock / JumpBlock / Baseblock / kuai11 等)
4. 使用工具栏 **画笔 / 框选 / 吸管 / 橡皮擦 / 填充**(快捷键 `B` `R` `I` `E` `F`
5. 选择 **Ground****Border** 层,在中间网格绘制
6. 绘制/改实体后 **自动保存** `levels-database.json`**自动烘焙** `Level{id}.prefab`,同步 `tools/level-prefab-index.json`
7.**资源管理器** 中编辑并 **保存** `Level{id}.prefab` 后,扩展会 **自动回写 JSON** 并刷新本面板(当前关卡 ID 一致时)
8. 需要时 **打开 Level 预制体** 预览,或点 **从预制体同步** 手动触发回写
## 预制体 ↔ JSON 双向同步
| 方向 | 触发 |
|------|------|
| JSON → prefab | 面板改地图后自动烘焙,或点「烘焙预制体」 |
| prefab → JSON | 保存 `level-prefabs/Level{id}.prefab` 后自动导入;或点「从预制体同步」 |
回写内容:`LevelMapData``groundJson` / `borderJson` / `theme`;若 Ground/Border 子节点(`g_x_y` / `b_x_y`)与 JSON 不一致,以 **场景瓦片节点** 为准。`spawns` / `boundary` 仍由关卡控制器维护,不会被 prefab 覆盖。
## 场景内格子吸附
- **GridSnapHelper** 组件(挂在 Level 根节点):拖动瓦片时自动吸附,可显示蓝色参考网格
- 面板 **启用场景吸附助手**:为当前 Level 添加该组件
- 面板 **场景选中吸附**:将场景中已选瓦片立即对齐到格子
- 画布鼠标移动时会 **高亮** 当前吸附格并显示坐标
## 数据与工具
| 路径 | 说明 |
|------|------|
| `assets/resources/level/levels-database.json` | 关卡逻辑数据 |
| `assets/resources/map-tiles/palettes/_index.json` | 主题调色板索引 |
| `assets/resources/level-prefabs/Level91601.prefab` | 烘焙后的关卡预制体ID 与 Unity 一致,首关 91601 |
| `tools/level-prefab-index.json` | 预制体路径索引(烘焙时自动重建) |
| `tools/level_id.py` / `dist/level-id.js` | 关卡 ID 约定91601 首关) |
生成调色板(首次或 Unity 资源更新后):
```bash
python3 tools/build_theme_palettes.py --unity-root "/path/to/主站"
```
详细说明见项目 `tools/level-map-editor.md`
## 平台
- Cocos Creator **3.8.x**
- macOS / Windows 编辑器

View File

@@ -0,0 +1,50 @@
'use strict';
const fs = require('fs');
const path = require('path');
const REL = 'temp/.lme-bake-ignore.json';
const DEFAULT_MS = 3500;
function ignoreFilePath(projectPath) {
return path.join(projectPath || Editor.Project.path, REL);
}
function readAll(projectPath) {
const fp = ignoreFilePath(projectPath);
try {
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
return data && typeof data === 'object' ? data : {};
} catch {
return {};
}
}
function writeAll(data, projectPath) {
const fp = ignoreFilePath(projectPath);
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.writeFileSync(fp, JSON.stringify(data), 'utf8');
}
function markBakePending(levelId, durationMs, projectPath) {
const ms = Number(durationMs) > 0 ? Number(durationMs) : DEFAULT_MS;
const data = readAll(projectPath);
data[String(levelId)] = Date.now() + ms;
writeAll(data, projectPath);
}
function shouldIgnoreImport(levelId, projectPath) {
const key = String(levelId);
const data = readAll(projectPath);
const until = data[key];
if (!until) return false;
if (until > Date.now()) return true;
delete data[key];
writeAll(data, projectPath);
return false;
}
module.exports = {
markBakePending,
shouldIgnoreImport,
};

View File

@@ -0,0 +1,153 @@
'use strict';
const TOOLS = ['paint', 'box', 'picker', 'eraser', 'fill'];
const TOOL_LABELS = {
paint: '画笔:在网格上点击或拖拽绘制(需先在右侧选瓦片)',
box: '框选:拖拽矩形区域,松开后用当前瓦片填满',
picker: '吸管:点击已有格子,吸取瓦片为当前画笔',
eraser: '橡皮擦:点击或拖拽删除当前层的格子',
fill: '油漆桶:填充相连的同类型格子(需先选瓦片)',
};
const TOOL_CURSORS = {
paint: 'crosshair',
box: 'crosshair',
picker: 'copy',
eraser: 'not-allowed',
fill: 'cell',
};
function layerMap(state) {
return state.editLayer === 'ground' ? state.config.ground : state.config.border;
}
function getCell(state, key) {
const map = layerMap(state);
return map[key];
}
function hasCell(state, key) {
return getCell(state, key) !== undefined;
}
function removeCell(state, key) {
if (state.editLayer === 'ground') delete state.config.ground[key];
else delete state.config.border[key];
}
function setCell(state, key, tileKey) {
if (state.editLayer === 'ground') state.config.ground[key] = tileKey;
else state.config.border[key] = tileKey;
}
function canPaintBrush(state) {
return state.brush && state.brush.layer === state.editLayer;
}
function fillRectangle(state, x0, y0, x1, y1, tileKey) {
const minX = Math.min(x0, x1);
const maxX = Math.max(x0, x1);
const minY = Math.min(y0, y1);
const maxY = Math.max(y0, y1);
let n = 0;
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
setCell(state, `${x},${y}`, tileKey);
n++;
}
}
return n;
}
function eraseRectangle(state, x0, y0, x1, y1) {
const minX = Math.min(x0, x1);
const maxX = Math.max(x0, x1);
const minY = Math.min(y0, y1);
const maxY = Math.max(y0, y1);
let n = 0;
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
const key = `${x},${y}`;
if (hasCell(state, key)) {
removeCell(state, key);
n++;
}
}
}
return n;
}
/** 四连通洪水填充(仅当前编辑层) */
function floodFill(state, startKey, fillTileKey) {
const map = layerMap(state);
const target = map[startKey];
if (target === fillTileKey) return 0;
const q = [startKey];
const seen = new Set([startKey]);
let n = 0;
while (q.length) {
const key = q.shift();
map[key] = fillTileKey;
n++;
const [xs, ys] = key.split(',');
const x = parseInt(xs, 10);
const y = parseInt(ys, 10);
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
const nk = `${nx},${ny}`;
if (seen.has(nk)) continue;
const v = map[nk];
const same = target === undefined ? v === undefined : v === target;
if (!same) continue;
seen.add(nk);
q.push(nk);
}
}
return n;
}
/** 洪水清除(橡皮擦油漆桶:删相连同值区域) */
function floodErase(state, startKey) {
const map = layerMap(state);
if (map[startKey] === undefined) return 0;
const target = map[startKey];
const q = [startKey];
const seen = new Set([startKey]);
let n = 0;
while (q.length) {
const key = q.shift();
delete map[key];
n++;
const [xs, ys] = key.split(',');
const x = parseInt(xs, 10);
const y = parseInt(ys, 10);
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
const nk = `${nx},${ny}`;
if (seen.has(nk) || map[nk] !== target) continue;
seen.add(nk);
q.push(nk);
}
}
return n;
}
function findPaletteTile(state, tileKey, layer) {
return state.tiles.find((t) => t.tileKey === tileKey && t.layer === layer) || null;
}
module.exports = {
TOOLS,
TOOL_LABELS,
TOOL_CURSORS,
layerMap,
getCell,
hasCell,
removeCell,
setCell,
canPaintBrush,
fillRectangle,
eraseRectangle,
floodFill,
floodErase,
findPaletteTile,
};

View File

@@ -0,0 +1,46 @@
'use strict';
/** 与 assets/scripts/level/EntitySpawnDefaults.ts 保持一致 */
const ENTITY_BASE_HEIGHT = {
player: 90,
vehicle: 80,
};
const ENTITY_BASE_SCALE = {
player: 1,
vehicle: 1,
prop: 0.85,
prop_decor: 0.6,
enemy: 1,
};
const DEFAULT_SPAWN_SCALE = {
player: 1,
vehicle: 1,
prop: 1,
};
function clampScale(v) {
const n = typeof v === 'number' ? v : parseFloat(String(v));
if (Number.isNaN(n)) return 1;
return Math.max(0.1, Math.min(4, n));
}
function normalizeSpawnScale(scale) {
const s = clampScale(scale);
return Math.abs(s - 1) < 0.001 ? undefined : s;
}
function resolveEntityScaleMul(kind, spawnScale) {
return clampScale(spawnScale !== undefined && spawnScale !== null ? spawnScale : 1);
}
module.exports = {
ENTITY_BASE_HEIGHT,
ENTITY_BASE_SCALE,
DEFAULT_SPAWN_SCALE,
clampScale,
normalizeSpawnScale,
resolveEntityScaleMul,
};

View File

@@ -0,0 +1,171 @@
'use strict';
/** 各主题推荐贴图路径resources 相对路径,无 .png供编辑器「从主题填充」 */
/** Prop_kuai1 → nProp_kuai1Prop → nProp */
function propToGroundPath(blockPath) {
if (!blockPath) return '';
const norm = String(blockPath).trim().replace(/\\/g, '/');
const slash = norm.lastIndexOf('/');
const dir = slash >= 0 ? norm.slice(0, slash + 1) : '';
const file = slash >= 0 ? norm.slice(slash + 1) : norm;
if (file.startsWith('Prop_')) return `${dir}n${file}`;
if (file === 'Prop') return `${dir}nProp`;
if (file.startsWith('nProp')) return norm;
return norm.replace(/\/Prop([^/]*)$/, '/nProp$1');
}
function withPropGround(preset) {
return {
...preset,
propGround: propToGroundPath(preset.prop),
};
}
function vehicleFourWay(folder, prefix) {
const base = `textures/${folder}/${prefix}`;
return {
vehicleNorth: `${base}_N`,
vehicleEast: `${base}_E`,
vehicleSouth: `${base}_S`,
vehicleWest: `${base}_W`,
};
}
const THEME_ENTITY_PRESETS = {
default: withPropGround({
playerFront: 'textures/default/player_F',
playerBack: 'textures/default/player_B',
...vehicleFourWay('default', 'ship'),
prop: 'textures/default/Prop',
}),
silu: withPropGround({
playerFront: 'textures/silu/skin/待机正面/1',
playerBack: 'textures/silu/skin/待机背面/1',
...vehicleFourWay('silu', 'siluShip'),
prop: 'textures/silu/Prop_kuai1',
}),
chinese: withPropGround({
playerFront: 'textures/chinese/chineseShip_F',
playerBack: 'textures/chinese/chineseShip_B',
...vehicleFourWay('chinese', 'chineseShip'),
prop: 'textures/chinese/Prop_kuai1',
}),
sanxing: withPropGround({
playerFront: 'textures/sanxing/skin/待机正面/1',
playerBack: 'textures/sanxing/skin/待机背面/1',
...vehicleFourWay('sanxing', 'sanxingShip'),
prop: 'textures/sanxing/Prop_kuai1',
}),
snow: withPropGround({
playerFront: 'textures/snow/skin/待机正面/1',
playerBack: 'textures/snow/skin/待机背面/1',
...vehicleFourWay('snow', 'snowShip'),
prop: 'textures/snow/Prop_kuai1',
}),
numMan: withPropGround({
playerFront: 'textures/numMan/skin/待机正面/1',
playerBack: 'textures/numMan/skin/待机背面/1',
...vehicleFourWay('numMan', 'numManShip'),
prop: 'textures/numMan/Prop_kuai11',
}),
redArmy: withPropGround({
playerFront: 'textures/redArmy/skin/待机正面/1',
playerBack: 'textures/redArmy/skin/待机背面/1',
...vehicleFourWay('redArmy', 'redArmyShip'),
prop: 'textures/redArmy/Prop_kuai1',
}),
redarmy: withPropGround({
playerFront: 'textures/redArmy/skin/待机正面/1',
playerBack: 'textures/redArmy/skin/待机背面/1',
...vehicleFourWay('redArmy', 'redArmyShip'),
prop: 'textures/redArmy/Prop_kuai1',
}),
};
const ENTITY_TEXTURE_FIELDS = [
{ key: 'playerFront', label: '角色正面' },
{ key: 'playerBack', label: '角色背面' },
{ key: 'vehicleNorth', label: '载具北' },
{ key: 'vehicleEast', label: '载具东' },
{ key: 'vehicleSouth', label: '载具南' },
{ key: 'vehicleWest', label: '载具西' },
{ key: 'prop', label: '可拾取物(砖块 Prop)' },
{ key: 'propGround', label: '可拾取物(空地 nProp)' },
];
function normalizeTexturePath(raw) {
if (!raw) return '';
let p = String(raw).trim().replace(/\\/g, '/');
if (p.startsWith('assets/resources/')) p = p.slice('assets/resources/'.length);
if (p.startsWith('resources/')) p = p.slice('resources/'.length);
if (p.startsWith('/')) p = p.slice(1);
if (p.endsWith('.png')) p = p.slice(0, -4);
return p;
}
function ensureEntityTextures(state) {
if (!state.config) return null;
if (!state.config.entityTextures || typeof state.config.entityTextures !== 'object') {
state.config.entityTextures = {};
}
return state.config.entityTextures;
}
function presetForTheme(theme) {
return THEME_ENTITY_PRESETS[theme] || THEME_ENTITY_PRESETS.silu;
}
function applyThemePreset(state, theme) {
const et = ensureEntityTextures(state);
if (!et) return false;
const preset = presetForTheme(theme || state.theme || 'silu');
Object.assign(et, { ...preset });
return true;
}
function readEntityTexturesFromPanel(listEl) {
const out = {};
for (const f of ENTITY_TEXTURE_FIELDS) {
const el = listEl?.querySelector(`#entity-tex-${f.key}`);
const v = normalizeTexturePath(el?.value || '');
if (v) out[f.key] = v;
}
return out;
}
function writeEntityTexturesToPanel(listEl, entityTextures) {
const et = entityTextures || {};
for (const f of ENTITY_TEXTURE_FIELDS) {
const el = listEl?.querySelector(`#entity-tex-${f.key}`);
if (el) el.value = et[f.key] || '';
}
}
function pruneEmptyEntityTextures(state) {
const et = state.config?.entityTextures;
if (!et) return;
let hasAny = false;
for (const f of ENTITY_TEXTURE_FIELDS) {
const v = normalizeTexturePath(et[f.key]);
if (v) {
et[f.key] = v;
hasAny = true;
} else {
delete et[f.key];
}
}
if (!hasAny) delete state.config.entityTextures;
}
module.exports = {
THEME_ENTITY_PRESETS,
ENTITY_TEXTURE_FIELDS,
normalizeTexturePath,
ensureEntityTextures,
presetForTheme,
applyThemePreset,
readEntityTexturesFromPanel,
writeEntityTexturesToPanel,
pruneEmptyEntityTextures,
};

View File

@@ -0,0 +1,87 @@
'use strict';
/** 与 Unity PPU=100、Grid CellSize (1,0.5,1) 一致(世界坐标 Y 向上) */
const CELL_PIXEL = 100;
const HALF_W = CELL_PIXEL * 0.5;
const HALF_H = CELL_PIXEL * 0.25;
const PANEL_HALF_W = HALF_W;
const PANEL_HALF_H = HALF_H;
function worldToCellXY(wx, wy) {
const cx = (wy / HALF_H + wx / HALF_W) * 0.5;
const cy = (wy / HALF_H - wx / HALF_W) * 0.5;
return { x: Math.round(cx), y: Math.round(cy) };
}
/** 世界坐标为格子中心时 → 格子索引(与 cellCenterCanvas 互逆) */
function worldCenterToCellXY(wx, wy) {
return worldToCellXY(wx, wy - HALF_H);
}
function cellToWorldXY(cx, cy) {
return {
x: (cx - cy) * HALF_W,
y: (cx + cy) * HALF_H,
};
}
function cellCenterWorldXY(cx, cy) {
const w = cellToWorldXY(cx, cy);
return { x: w.x, y: w.y + HALF_H };
}
function cellKey(x, y) {
return `${x},${y}`;
}
/** 世界坐标 → CanvasY 向下,与 Unity/Cocos 预览一致) */
function worldToCanvasXY(wx, wy, offsetX, offsetY) {
return {
x: offsetX + wx,
y: offsetY - wy,
};
}
/** 格子底顶点Grid.CellToWorld */
function cellAnchorCanvas(cx, cy, offsetX, offsetY) {
const w = cellToWorldXY(cx, cy);
return worldToCanvasXY(w.x, w.y, offsetX, offsetY);
}
/** 格子中心Unity Tilemap m_TileAnchor 0.5,0.5 → 精灵 pivot 对齐点) */
function cellCenterCanvas(cx, cy, offsetX, offsetY) {
const w = cellToWorldXY(cx, cy);
return worldToCanvasXY(w.x, w.y + HALF_H, offsetX, offsetY);
}
/** 鼠标拾取:与贴图 pivot 相同使用格子中心worldCenterToCell */
function cellFromCanvas(mx, my, offsetX, offsetY) {
const wx = mx - offsetX;
const wyCenter = offsetY - my;
const c = worldCenterToCellXY(wx, wyCenter);
return { x: c.x, y: c.y, key: cellKey(c.x, c.y) };
}
function cellToCanvas(cx, cy, offsetX, offsetY) {
const a = cellAnchorCanvas(cx, cy, offsetX, offsetY);
return { px: a.x, py: a.y };
}
module.exports = {
CELL_PIXEL,
HALF_W,
HALF_H,
PANEL_HALF_W,
PANEL_HALF_H,
worldToCellXY,
worldCenterToCellXY,
cellToWorldXY,
cellCenterWorldXY,
cellKey,
worldToCanvasXY,
cellAnchorCanvas,
cellCenterCanvas,
cellFromCanvas,
cellToCanvas,
};

View File

@@ -0,0 +1,109 @@
'use strict';
/** 与 Unity / 主站 config.js BEGINNING_REAL_LVID、LevelRegistry.LEVEL_ID_BASE 一致 */
const LEVEL_ID_BASE = 91601;
const PREFAB_DIR = 'level-prefabs';
/** internalIndex 0 → 91601首关 */
function externalLevelId(internalIndex) {
return LEVEL_ID_BASE + internalIndex;
}
function internalLevelIndex(levelId) {
return levelId >= LEVEL_ID_BASE ? levelId - LEVEL_ID_BASE : levelId;
}
function isGameLevelId(levelId) {
return levelId >= LEVEL_ID_BASE;
}
function isExternalLevelId(levelId) {
return isGameLevelId(levelId);
}
function minLevelId() {
return LEVEL_ID_BASE;
}
function prefabResourcePath(levelId) {
return `${PREFAB_DIR}/Level${levelId}`;
}
function normalizeDb(db) {
if (!db.levels) db.levels = {};
db.levelIdBase = LEVEL_ID_BASE;
return db;
}
function nextAvailableLevelId(db) {
const keys = Object.keys(db.levels || {});
if (!keys.length) return LEVEL_ID_BASE;
const ids = keys.map((k) => parseInt(k, 10)).filter((n) => !Number.isNaN(n));
return Math.max(...ids) + 1;
}
function syncLevelEntry(cfg, levelId) {
const out = { ...cfg };
out.levelID = levelId;
out.cocosPrefab = prefabResourcePath(levelId);
if (typeof out.unityPrefab === 'string' && out.unityPrefab) {
out.unityPrefab = out.unityPrefab.replace(/Level\d+\.prefab$/i, `Level${levelId}.prefab`);
}
return out;
}
function updateDbStats(db) {
const total = Object.keys(db.levels || {}).length;
db.stats = { ...(db.stats || {}), total, withPrefabTilemap: total };
}
function touchDatabase(db, levelId) {
normalizeDb(db);
const key = String(levelId);
if (db.levels[key]) {
db.levels[key] = syncLevelEntry(db.levels[key], levelId);
}
updateDbStats(db);
db.generatedAt = new Date().toISOString();
}
function sortedLevelIds(db) {
return Object.keys(db.levels || {})
.map((k) => parseInt(k, 10))
.filter((n) => !Number.isNaN(n))
.sort((a, b) => a - b);
}
function prevLevelIdInDb(db, cur) {
const ids = sortedLevelIds(db);
if (!ids.length) return cur;
const i = ids.indexOf(cur);
if (i < 0) return ids[0];
return ids[(i - 1 + ids.length) % ids.length];
}
function nextLevelIdInDb(db, cur) {
const ids = sortedLevelIds(db);
if (!ids.length) return cur;
const i = ids.indexOf(cur);
if (i < 0) return ids[0];
return ids[(i + 1) % ids.length];
}
module.exports = {
LEVEL_ID_BASE,
PREFAB_DIR,
externalLevelId,
internalLevelIndex,
isExternalLevelId,
minLevelId,
prefabResourcePath,
normalizeDb,
nextAvailableLevelId,
syncLevelEntry,
updateDbStats,
touchDatabase,
sortedLevelIds,
prevLevelIdInDb,
nextLevelIdInDb,
};

164
extensions/level-map-editor/dist/main.js vendored Normal file
View File

@@ -0,0 +1,164 @@
'use strict';
const fs = require('fs');
const path = require('path');
const prefabSync = require('./prefab-sync');
const bakeIgnore = require('./bake-ignore');
const IMPORT_DEBOUNCE_MS = 900;
/** levelId → debounce timer */
const importTimers = new Map();
let prefabWatcher = null;
let broadcastUnsubs = [];
function addBroadcastListener(message, handler) {
const fn = Editor.Message.__protected__?.addBroadcastListener || Editor.Message.addBroadcastListener;
if (typeof fn !== 'function') {
console.warn(`[level-map-editor] addBroadcastListener unavailable for ${message}`);
return null;
}
fn.call(Editor.Message, message, handler);
return () => {
const rm = Editor.Message.__protected__?.removeBroadcastListener || Editor.Message.removeBroadcastListener;
if (typeof rm === 'function') rm.call(Editor.Message, message, handler);
};
}
function scheduleImportFromPrefab(levelId, source) {
if (!levelId || bakeIgnore.shouldIgnoreImport(levelId)) return;
if (importTimers.has(levelId)) clearTimeout(importTimers.get(levelId));
importTimers.set(
levelId,
setTimeout(() => {
importTimers.delete(levelId);
runImportFromPrefab(levelId, source);
}, IMPORT_DEBOUNCE_MS),
);
}
function runImportFromPrefab(levelId, source) {
if (bakeIgnore.shouldIgnoreImport(levelId)) return null;
try {
const result = prefabSync.importLevelFromPrefab(levelId);
if (!result.ok) {
console.warn(`[level-map-editor] prefab import skipped Level${levelId}: ${result.reason}`);
return result;
}
console.log(
`[level-map-editor] prefab → JSON Level${levelId} (ground ${result.ground}, border ${result.border}, theme ${result.theme || '-'}) [${source}]`,
);
void prefabSync.refreshDatabaseAsset();
Editor.Message.broadcast('level-map-editor:prefab-synced', {
levelId,
source,
ground: result.ground,
border: result.border,
theme: result.theme,
});
return result;
} catch (e) {
console.error(`[level-map-editor] prefab import failed Level${levelId}`, e);
return { ok: false, reason: e.message, levelId };
}
}
async function handleAssetDbEvent(...args) {
let url = '';
for (const arg of args) {
if (typeof arg === 'string') {
if (arg.includes('level-prefabs/Level') && arg.endsWith('.prefab')) url = arg;
else if (arg.startsWith('db://') && arg.includes('Level') && arg.endsWith('.prefab')) url = arg;
} else if (arg && typeof arg === 'object') {
if (typeof arg.url === 'string') url = arg.url;
else if (typeof arg.path === 'string') url = arg.path;
}
}
if (!url && typeof args[0] === 'string' && !args[0].includes('/')) {
try {
url = await Editor.Message.request('asset-db', 'query-url', args[0]);
} catch {
/* ignore */
}
}
const levelId = prefabSync.extractLevelIdFromUrl(url);
if (levelId) scheduleImportFromPrefab(levelId, 'asset-db');
}
function watchPrefabDirectory() {
const dir = path.join(Editor.Project.path, prefabSync.PREFAB_DIR_REL);
if (!fs.existsSync(dir)) return;
try {
prefabWatcher = fs.watch(dir, (_event, filename) => {
const levelId = prefabSync.extractLevelIdFromFilename(filename);
if (levelId) scheduleImportFromPrefab(levelId, 'fs-watch');
});
} catch (e) {
console.warn('[level-map-editor] prefab fs.watch failed', e);
}
}
function registerAssetListeners() {
const events = [
'asset-db:asset-change',
'asset-db:asset-changed',
'asset-db:assets-changed',
'asset-db:asset-add',
'asset-db:assets-created',
];
for (const message of events) {
const unsub = addBroadcastListener(message, (...args) => {
void handleAssetDbEvent(...args);
});
if (unsub) broadcastUnsubs.push(unsub);
}
}
exports.methods = {
openPanel() {
const id = 'level-map-editor';
try {
Editor.Panel.open(id);
console.log(`[${id}] Panel.open('${id}')`);
} catch (e) {
console.error(`[${id}] open failed:`, e);
}
},
markBakePending(levelId, durationMs) {
bakeIgnore.markBakePending(levelId, durationMs);
},
importLevelFromPrefab(levelId) {
return runImportFromPrefab(Number(levelId), 'manual');
},
};
exports.load = function () {
prefabSync.clearSpriteUuidCache();
registerAssetListeners();
watchPrefabDirectory();
console.log('[level-map-editor] extension loaded (v1.7.1, prefab→JSON sync enabled)');
};
exports.unload = function () {
for (const unsub of broadcastUnsubs) {
try {
unsub();
} catch {
/* ignore */
}
}
broadcastUnsubs = [];
if (prefabWatcher) {
try {
prefabWatcher.close();
} catch {
/* ignore */
}
prefabWatcher = null;
}
for (const timer of importTimers.values()) clearTimeout(timer);
importTimers.clear();
console.log('[level-map-editor] extension unloaded');
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,271 @@
'use strict';
const fs = require('fs');
const path = require('path');
const levelIdUtil = require('./level-id');
const LEVEL_MAP_TYPE = 'd4e5fanuMlNDh8qO0xdbn+K';
const DB_REL = 'assets/level-data/levels-database.json';
const PREFAB_DIR_REL = 'assets/resources/level-prefabs';
const PREFAB_NAME_RE = /^Level(\d+)\.prefab$/i;
const TILE_NODE_RE = /^([gb])_(-?\d+)_(-?\d+)$/;
let spriteUuidCache = null;
function projectRoot(projectPath) {
return projectPath || Editor.Project.path;
}
function dbPath(projectPath) {
return path.join(projectRoot(projectPath), DB_REL);
}
function prefabDir(projectPath) {
return path.join(projectRoot(projectPath), PREFAB_DIR_REL);
}
function prefabPath(levelId, projectPath) {
return path.join(prefabDir(projectPath), `Level${levelId}.prefab`);
}
function readJsonFile(fp, fallback) {
try {
return JSON.parse(fs.readFileSync(fp, 'utf8'));
} catch {
return fallback;
}
}
function writeJsonFile(fp, data) {
fs.mkdirSync(path.dirname(fp), { recursive: true });
fs.writeFileSync(fp, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
}
function buildSpriteUuidCache(projectPath) {
if (spriteUuidCache) return spriteUuidCache;
const cache = new Map();
const texRoot = path.join(projectRoot(projectPath), 'assets/resources/textures');
const walk = (dir) => {
if (!fs.existsSync(dir)) return;
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, ent.name);
if (ent.isDirectory()) {
walk(full);
continue;
}
if (!/\.(png|jpg)\.meta$/i.test(ent.name)) continue;
const tileKey = ent.name.replace(/\.(png|jpg)\.meta$/i, '');
try {
const meta = JSON.parse(fs.readFileSync(full, 'utf8'));
if (meta.subMetas) {
for (const sub of Object.values(meta.subMetas)) {
if (sub && sub.uuid) cache.set(sub.uuid, tileKey);
}
}
if (meta.uuid) {
cache.set(meta.uuid, tileKey);
cache.set(`${meta.uuid}@f9941`, tileKey);
}
} catch {
/* ignore broken meta */
}
}
};
walk(texRoot);
spriteUuidCache = cache;
return cache;
}
function tileKeyFromSpriteUuid(uuid, projectPath) {
if (!uuid) return null;
const cache = buildSpriteUuidCache(projectPath);
const direct = cache.get(uuid);
if (direct) return direct;
const base = String(uuid).split('@')[0];
for (const [key, val] of cache.entries()) {
if (key.startsWith(base)) return val;
}
return null;
}
function parseJsonRecord(raw) {
try {
const parsed = JSON.parse(raw || '{}');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function parseLevelMapData(objs) {
for (const obj of objs) {
if (!obj || typeof obj !== 'object') continue;
if (obj.__type__ !== LEVEL_MAP_TYPE && obj.groundJson === undefined) continue;
if (obj.groundJson === undefined && obj.levelID === undefined) continue;
return {
levelID: parseInt(obj.levelID, 10) || 0,
ground: parseJsonRecord(obj.groundJson),
border: parseJsonRecord(obj.borderJson),
theme: String(obj.theme || '').trim() || null,
};
}
return null;
}
function spriteUuidOnNode(objs, node) {
if (!node || !Array.isArray(node._components)) return null;
for (const ref of node._components) {
const comp = objs[(ref && ref.__id__) - 1];
if (!comp || comp.__type__ !== 'cc.Sprite') continue;
const frame = comp._spriteFrame;
if (frame && frame.__uuid__) return frame.__uuid__;
}
return null;
}
function parseTilesFromNodes(objs, projectPath) {
const ground = {};
const border = {};
for (const obj of objs) {
if (!obj || obj.__type__ !== 'cc.Node' || typeof obj._name !== 'string') continue;
const m = obj._name.match(TILE_NODE_RE);
if (!m) continue;
const x = parseInt(m[2], 10);
const y = parseInt(m[3], 10);
if (Number.isNaN(x) || Number.isNaN(y)) continue;
const key = `${x},${y}`;
const layer = m[1] === 'g' ? 'ground' : 'border';
const tileKey = tileKeyFromSpriteUuid(spriteUuidOnNode(objs, obj), projectPath);
if (layer === 'ground') {
ground[key] = tileKey || 'Baseblock';
} else {
border[key] = tileKey || 'WallBlock';
}
}
return { ground, border };
}
function mergeMapSources(mapData, fromNodes) {
const md = mapData || { ground: {}, border: {}, theme: null, levelID: 0 };
const nodes = fromNodes || { ground: {}, border: {} };
const ground = Object.keys(nodes.ground).length ? nodes.ground : md.ground || {};
const border = Object.keys(nodes.border).length ? nodes.border : md.border || {};
return {
levelID: md.levelID || 0,
ground,
border,
theme: md.theme || null,
};
}
function mergeEntry(levelId, mapData, prev) {
const extId = levelIdUtil.isExternalLevelId(levelId) ? levelId : levelIdUtil.externalLevelId(levelId);
const out = {
levelID: extId,
boundary: (prev && prev.boundary) || { x: 20, y: 20 },
spawns: Array.isArray(prev && prev.spawns) ? [...prev.spawns] : [],
cocosPrefab: levelIdUtil.prefabResourcePath(extId),
};
if (prev && prev.unityPrefab) out.unityPrefab = prev.unityPrefab;
if (mapData) {
if (mapData.ground && Object.keys(mapData.ground).length) out.ground = mapData.ground;
if (mapData.border && Object.keys(mapData.border).length) out.border = mapData.border;
if (mapData.theme) out.theme = mapData.theme;
if (mapData.levelID > 0) out.levelID = levelIdUtil.isExternalLevelId(mapData.levelID) ? mapData.levelID : levelIdUtil.externalLevelId(mapData.levelID);
} else if (prev) {
for (const k of ['ground', 'border', 'theme', 'entityTextures']) {
if (prev[k] !== undefined) out[k] = prev[k];
}
}
return out;
}
function parsePrefabFile(prefabFile, projectPath) {
const objs = readJsonFile(prefabFile, null);
if (!Array.isArray(objs)) return null;
const mapData = parseLevelMapData(objs);
const fromNodes = parseTilesFromNodes(objs, projectPath);
return mergeMapSources(mapData, fromNodes);
}
function loadDatabase(projectPath) {
const fp = dbPath(projectPath);
if (!fs.existsSync(fp)) {
return levelIdUtil.normalizeDb({ levels: {} });
}
return levelIdUtil.normalizeDb(readJsonFile(fp, { levels: {} }));
}
function saveDatabase(db, projectPath) {
levelIdUtil.normalizeDb(db);
levelIdUtil.updateDbStats(db);
db.generatedAt = new Date().toISOString();
if (!db.version) db.version = 2;
if (!db.source) db.source = 'Cocos level-prefabs LevelMapData + levels-database spawns';
writeJsonFile(dbPath(projectPath), db);
}
async function refreshDatabaseAsset() {
const assetUrl = `db://${DB_REL}`;
try {
await Editor.Message.request('asset-db', 'refresh-asset', assetUrl);
const uuid = await Editor.Message.request('asset-db', 'query-uuid', assetUrl);
if (uuid) await Editor.Message.request('asset-db', 'refresh-asset', uuid);
} catch (e) {
console.warn('[level-map-editor] levels-database refresh failed', e);
}
}
function importLevelFromPrefab(levelId, options = {}) {
const projectPath = projectRoot(options.projectPath);
const fp = prefabPath(levelId, projectPath);
if (!fs.existsSync(fp)) {
return { ok: false, reason: 'prefab missing', levelId };
}
const mapData = parsePrefabFile(fp, projectPath);
if (!mapData) {
return { ok: false, reason: 'parse failed', levelId };
}
const db = loadDatabase(projectPath);
const key = String(levelId);
const prev = db.levels[key];
db.levels[key] = mergeEntry(levelId, mapData, prev);
levelIdUtil.touchDatabase(db, levelId);
saveDatabase(db, projectPath);
return {
ok: true,
levelId,
ground: Object.keys(mapData.ground || {}).length,
border: Object.keys(mapData.border || {}).length,
theme: mapData.theme,
};
}
function extractLevelIdFromUrl(url) {
if (!url || typeof url !== 'string') return null;
const m = url.match(/level-prefabs\/Level(\d+)\.prefab/i);
return m ? parseInt(m[1], 10) : null;
}
function extractLevelIdFromFilename(name) {
if (!name || typeof name !== 'string') return null;
const m = name.match(PREFAB_NAME_RE);
return m ? parseInt(m[1], 10) : null;
}
module.exports = {
DB_REL,
PREFAB_DIR_REL,
prefabPath,
dbPath,
parsePrefabFile,
importLevelFromPrefab,
refreshDatabaseAsset,
extractLevelIdFromUrl,
extractLevelIdFromFilename,
clearSpriteUuidCache() {
spriteUuidCache = null;
},
};

View File

@@ -0,0 +1,86 @@
'use strict';
const grid = require('./grid-math');
function findNodeByUuid(root, uuid) {
if (!root || !uuid) return null;
if (root.uuid === uuid) return root;
for (const ch of root.children) {
const hit = findNodeByUuid(ch, uuid);
if (hit) return hit;
}
return null;
}
function resolveLayer(node) {
let cur = node;
while (cur) {
if (cur.name === 'Ground') return 'ground';
if (cur.name === 'Border') return 'border';
cur = cur.parent;
}
const m = node.name && node.name.match(/^([gb])_(-?\d+)_(-?\d+)$/);
if (m) return m[1] === 'g' ? 'ground' : 'border';
return null;
}
exports.methods = {
/**
* 将指定 uuid 节点吸附到等距格子中心(场景内拖动后调用)
*/
snapNodeByUuid(uuid) {
const { director, Vec3 } = require('cc');
const scene = director.getScene();
if (!scene) return { ok: false, reason: 'no scene' };
const node = findNodeByUuid(scene, uuid);
if (!node) return { ok: false, reason: 'node not found' };
const layer = resolveLayer(node);
if (!layer) return { ok: false, reason: 'not under Ground/Border' };
const p = node.position;
const c = grid.worldCenterToCellXY(p.x, p.y);
const w = grid.cellCenterWorldXY(c.x, c.y);
node.setPosition(new Vec3(w.x, w.y, p.z));
const prefix = layer === 'ground' ? 'g' : 'b';
const expected = `${prefix}_${c.x}_${c.y}`;
if (/^[gb]_-?\d+_-?\d+$/.test(node.name) || node.name.startsWith(prefix)) {
node.name = expected;
}
return { ok: true, cell: c, key: grid.cellKey(c.x, c.y), layer };
},
/** 批量吸附(参数为 uuid 数组) */
snapNodes(uuids) {
const list = Array.isArray(uuids) ? uuids : [uuids];
const results = [];
for (const id of list) {
results.push(exports.methods.snapNodeByUuid(id));
}
return results;
},
/** 在 Level 根节点挂载 GridSnapHelper编辑器场景内持续吸附 */
attachHelperOnSelection(uuid) {
const { director } = require('cc');
const scene = director.getScene();
if (!scene) return { ok: false, reason: 'no scene' };
let node = findNodeByUuid(scene, uuid);
if (!node) return { ok: false, reason: 'node not found' };
let root = node;
while (root.parent && !/^Level\d+$/.test(root.name)) {
root = root.parent;
}
if (!/^Level\d+$/.test(root.name)) {
return { ok: false, reason: 'not a Level prefab node' };
}
let comp = root.getComponent('GridSnapHelper');
if (!comp) {
comp = root.addComponent('GridSnapHelper');
}
return { ok: true, level: root.name, hasHelper: !!comp };
},
};
exports.load = function () {};
exports.unload = function () {};

View File

@@ -0,0 +1,252 @@
'use strict';
/** 与 Unity Levels*.cs / levels-database.json spawns 一致 */
const { normalizeSpawnScale, clampScale, DEFAULT_SPAWN_SCALE } = require('./entity-spawn-defaults');
const { normalizeTexturePath } = require('./entity-texture-presets');
const SPAWN_KIND_LABELS = {
player: '玩家',
prop: '可拾取物',
vehicle: '载具',
};
const DIRECTIONS = [
'Direction.North',
'Direction.East',
'Direction.South',
'Direction.West',
];
function ensureSpawns(state) {
if (!state.config) return [];
if (!Array.isArray(state.config.spawns)) state.config.spawns = [];
return state.config.spawns;
}
function spawnsAt(state, x, y) {
return ensureSpawns(state).filter((s) => s.x === x && s.y === y);
}
function findSpawnAt(state, x, y) {
return spawnsAt(state, x, y)[0] || null;
}
function countByKind(state, kind) {
return ensureSpawns(state).filter((s) => s.kind === kind).length;
}
function getPlayerSpawn(state) {
return ensureSpawns(state).find((s) => s.kind === 'player') || null;
}
function getVehicleSpawn(state) {
return ensureSpawns(state).find((s) => s.kind === 'vehicle') || null;
}
function applyScaleField(entry, scale) {
const s = normalizeSpawnScale(scale);
if (s !== undefined) entry.scale = s;
else delete entry.scale;
}
function setPlayerSpawn(state, x, y, direction, scale) {
const prev = getPlayerSpawn(state);
const rest = ensureSpawns(state).filter((s) => s.kind !== 'player');
const entry = {
x,
y,
kind: 'player',
playerDirection: direction || 'Direction.South',
};
const sc = scale !== undefined ? scale : prev?.scale ?? DEFAULT_SPAWN_SCALE.player;
applyScaleField(entry, sc);
rest.push(entry);
state.config.spawns = rest;
return entry;
}
function setVehicleSpawn(state, x, y, direction, scale) {
const prev = getVehicleSpawn(state);
const rest = ensureSpawns(state).filter((s) => s.kind !== 'vehicle');
const entry = {
x,
y,
kind: 'vehicle',
vehicleDirection: direction || 'Direction.North',
};
const sc = scale !== undefined ? scale : prev?.scale ?? DEFAULT_SPAWN_SCALE.vehicle;
applyScaleField(entry, sc);
rest.push(entry);
state.config.spawns = rest;
return entry;
}
function clearVehicleSpawn(state) {
const before = countByKind(state, 'vehicle');
state.config.spawns = ensureSpawns(state).filter((s) => s.kind !== 'vehicle');
return before > 0;
}
/** 载具 0 或 1再次点击已有载具格则清除 */
function vehicleToggleInternal(state, x, y, direction, scale) {
const existing = getVehicleSpawn(state);
if (existing && existing.x === x && existing.y === y) {
clearVehicleSpawn(state);
return { changed: false, spawn: null, removed: true };
}
const spawn = setVehicleSpawn(state, x, y, direction, scale);
return { changed: true, spawn, removed: false };
}
function toggleVehicleSpawn(state, x, y, direction, scale) {
const r = vehicleToggleInternal(state, x, y, direction, scale);
return r.changed;
}
function toggleVehicleSpawnDetailed(state, x, y, direction, scale) {
return vehicleToggleInternal(state, x, y, direction, scale);
}
function togglePropSpawn(state, x, y, scale, propPlacement) {
const spawns = ensureSpawns(state);
const idx = spawns.findIndex((s) => s.x === x && s.y === y && s.kind === 'prop');
if (idx >= 0) {
spawns.splice(idx, 1);
return { changed: false, spawn: null, removed: true };
}
const entry = { x, y, kind: 'prop' };
if (propPlacement === 'ground') entry.propPlacement = 'ground';
const sc = scale !== undefined ? scale : DEFAULT_SPAWN_SCALE.prop;
applyScaleField(entry, sc);
spawns.push(entry);
return { changed: true, spawn: entry, removed: false };
}
function removeSpawnAt(state, x, y) {
const spawns = ensureSpawns(state);
const before = spawns.length;
state.config.spawns = spawns.filter((s) => !(s.x === x && s.y === y));
return state.config.spawns.length < before;
}
function removeSpawnRef(state, spawnRef) {
if (!spawnRef) return false;
const spawns = ensureSpawns(state);
const idx = spawns.indexOf(spawnRef);
if (idx < 0) return false;
spawns.splice(idx, 1);
return true;
}
function pickSpawnAtCell(state, x, y, preferKind) {
const hits = spawnsAt(state, x, y);
if (!hits.length) return null;
if (preferKind) {
const m = hits.find((s) => s.kind === preferKind);
if (m) return m;
}
return hits[0];
}
function spawnStillExists(state, spawnRef) {
if (!spawnRef) return false;
return ensureSpawns(state).includes(spawnRef);
}
function updateSpawnEntry(state, spawnRef, patch) {
const spawns = ensureSpawns(state);
const idx = spawns.indexOf(spawnRef);
if (idx < 0) return false;
const cur = { ...spawns[idx] };
if (patch.x !== undefined) cur.x = parseInt(patch.x, 10);
if (patch.y !== undefined) cur.y = parseInt(patch.y, 10);
if (patch.playerDirection !== undefined) cur.playerDirection = patch.playerDirection;
if (patch.vehicleDirection !== undefined) cur.vehicleDirection = patch.vehicleDirection;
if (patch.scale !== undefined) applyScaleField(cur, patch.scale);
if (patch.texture !== undefined) {
const t = normalizeTexturePath(patch.texture);
if (t) cur.texture = t;
else delete cur.texture;
}
spawns[idx] = cur;
return true;
}
function formatSpawnScale(spawn) {
if (spawn?.scale !== undefined && spawn.scale !== null) return spawn.scale;
return 1;
}
function validateSpawns(state) {
const spawns = ensureSpawns(state);
const players = spawns.filter((s) => s.kind === 'player');
const vehicles = spawns.filter((s) => s.kind === 'vehicle');
const props = spawns.filter((s) => s.kind === 'prop');
const errors = [];
if (players.length !== 1) {
errors.push(`玩家必须恰好 1 个(当前 ${players.length}`);
}
if (vehicles.length > 1) {
errors.push(`载具最多 1 个(当前 ${vehicles.length}`);
}
if (props.length < 1) {
errors.push(`可拾取物至少 1 个(当前 ${props.length}`);
}
return { ok: errors.length === 0, errors, players, vehicles, props };
}
function spawnSummary(state) {
const v = validateSpawns(state);
const player = v.players[0];
const vehicle = v.vehicles[0];
const parts = [];
if (player) {
const sc = formatSpawnScale(player);
parts.push(`玩家 (${player.x},${player.y}) ${player.playerDirection || ''}${sc !== 1 ? ` ×${sc}` : ''}`);
} else {
parts.push('玩家 未设置');
}
parts.push(`载具 ${v.vehicles.length}/1`);
if (vehicle) {
const sc = formatSpawnScale(vehicle);
parts.push(`@(${vehicle.x},${vehicle.y}) ${vehicle.vehicleDirection || ''}${sc !== 1 ? ` ×${sc}` : ''}`);
}
parts.push(`可拾取物 ${v.props.length}`);
if (!v.ok) parts.push('[待完善]');
return parts.join(' · ');
}
function validationHint(state) {
const v = validateSpawns(state);
if (v.ok) return '';
return v.errors.join('');
}
module.exports = {
SPAWN_KIND_LABELS,
DIRECTIONS,
DEFAULT_SPAWN_SCALE,
clampScale,
ensureSpawns,
spawnsAt,
findSpawnAt,
countByKind,
getPlayerSpawn,
getVehicleSpawn,
setPlayerSpawn,
setVehicleSpawn,
clearVehicleSpawn,
toggleVehicleSpawn,
toggleVehicleSpawnDetailed,
togglePropSpawn,
removeSpawnAt,
removeSpawnRef,
pickSpawnAtCell,
spawnStillExists,
updateSpawnEntry,
formatSpawnScale,
validateSpawns,
spawnSummary,
validationHint,
};

View File

@@ -0,0 +1,73 @@
'use strict';
const fs = require('fs');
const path = require('path');
/** Unity spritePivot + 贴图尺寸PPU=100— 无主题数据时的回退 */
const TILE_META = {
Baseblock: { width: 101, height: 80, pivotX: 0.5, pivotY: 0.92 },
JumpBlock: { width: 101, height: 99, pivotX: 0.5, pivotY: 0.77 },
WallBlock: { width: 101, height: 115, pivotX: 0.5, pivotY: 0.67 },
kuai11: { width: 101, height: 74, pivotX: 0.5, pivotY: 1.01 },
};
let themeTileMeta = null;
function loadThemeTileMeta() {
if (themeTileMeta) return themeTileMeta;
try {
const fp = path.join(Editor.Project.path, 'assets/resources/theme/tile-display-meta.json');
if (fs.existsSync(fp)) {
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
themeTileMeta = data.themes || {};
return themeTileMeta;
}
} catch (e) {
console.warn('[tile-meta] 读取 tile-display-meta.json 失败', e);
}
themeTileMeta = {};
return themeTileMeta;
}
function normalizeTheme(theme) {
if (!theme) return undefined;
if (theme === 'redArmy') return 'redarmy';
return theme;
}
function getTileMeta(tileName, theme) {
const key = normalizeTheme(theme);
if (key) {
const themes = loadThemeTileMeta();
const entry = themes[key]?.[tileName];
if (entry) {
return {
width: entry.width,
height: entry.height,
pivotX: entry.pivotX,
pivotY: entry.pivotY,
};
}
}
return TILE_META[tileName] || TILE_META.Baseblock;
}
/**
* 格子中心 + Unity spritePivot宽度贴满菱形格保留高度供等距遮挡。
*/
function spriteDrawRect(anchorX, anchorY, imgW, imgH, meta, halfW) {
const hw = halfW || 50;
const srcW = imgW || meta.width;
const srcH = imgH || meta.height;
const scale = (2 * hw) / srcW;
const w = srcW * scale;
const h = srcH * scale;
return {
x: anchorX - w * meta.pivotX,
y: anchorY - (1 - meta.pivotY) * h,
w,
h,
};
}
module.exports = { TILE_META, getTileMeta, spriteDrawRect, loadThemeTileMeta };

View File

@@ -0,0 +1,53 @@
{
"package_version": 2,
"name": "level-map-editor",
"version": "1.7.1",
"description": "关卡地图编辑(对齐 Unity Tile Palette中间关卡、右侧地图模块",
"author": "tfrh",
"editor": ">=3.8.0",
"main": "./dist/main.js",
"panels": {
"default": {
"title": "关卡控制器",
"type": "dockable",
"main": "./dist/panels/level-map-editor.js",
"size": {
"min-width": 720,
"min-height": 480,
"width": 960,
"height": 640
}
}
},
"contributions": {
"menu": [
{
"path": "i18n:menu.panel/level-map-editor",
"label": "关卡控制器",
"message": "open-panel"
},
{
"path": "扩展",
"label": "关卡控制器",
"message": "open-panel"
}
],
"messages": {
"open-panel": {
"methods": ["openPanel"]
},
"mark-bake-pending": {
"methods": ["markBakePending"]
},
"import-level-from-prefab": {
"methods": ["importLevelFromPrefab"]
},
"level-map-editor:prefab-synced": {
"methods": []
}
},
"scene": {
"script": "./dist/scene.js"
}
}
}

View File

@@ -0,0 +1,352 @@
# Localization Editor API
## Quick Start
### [Core Features `l10n`](#l10n)
l10n provides the core translation function and icu function, as well as the function to switch languages.
We will store the switched target language in `localStorage` and also automatically restart the project runtime and read the `localStorage` configuration at the next start to complete the whole language switching process.
> ***So we want users to make sure to handle data persistence before switching languages***
### Import the [`l10n`](#l10n) module
All api's provided by localization-editor will be imported by name from db://localization-editor/l10n
```typescript
import { l10n } from 'db://localization-editor/l10n'
```
### Use the translation api
```typescript
// in the code of any component component
// l10n is the core functionality of localization
import { l10n } from 'db://localization-editor/l10n'
import { _decorator, Label, Component } from 'cc';
@ccclass('SomeComponent')
class SomeComponent extends Component {
// ......
someMethod() {
// will return the text corresponding to this_is_an_apple
const text = l10n.t("this_is_an_apple")
}
// ......
}
```
### API details
- Class:[`L10nManager`](#l10nmanager)
---
- Interface:[`ResourceList`](#resourcelist)
- Interface:[`ResourceBundle`](#resourcebundle)
- Interface:[`ResourceData`](#resourcedata)
- Interface:[`ResourceItem`](#resourceitem)
- Interface:[`FallbackLanguageObjectList`](#fallbacklanguageobjectlist)
- Interface:[`L10nOptions`](#l10noptions)
- Interface:[`StandardOption`](#standardoption)
---
- Enumerations:[`L10nListenEvent`](#l10nlistenevent)
---
- Alias:[`L10nKey`](#alternate name)
- Alias:[`L10nValue`](#alternate)
- Alias:[`TextInfoDirection`](# alias)
- Alias:[`FallbackLanguage`](# alias)
---
# `L10nManager`
Import example.
```ts
import { L10nManager } from 'db://localization-editor/l10n'
```
Description.
Normally we do not recommend that you use or construct this type yourself.
Instead, we provide [``l10n``](#l10n) as a global singleton to use the translation functionality.
---
## Index
### Constructor
- `L10nManager` **private**
---
### Global variables
#### `l10n`
Definition: `const l10n: L10nManager`
---
### Static properties
#### `LOCAL_STORAGE_LANGUAGE_KEY`
Definition: `static LOCAL_STORAGE_LANGUAGE_KEY: string`
Description: When calling [`changeLanguage`](#changelanguage) to switch the game language, `localStorage` is used to
and use [`LOCAL_STORAGE_LANGUAGE_KEY`](#localstoragelanguagekey) as the key of `localStorage`.
Remarks.
| default | localization-editor/language |
|-----|------------------------------|
---
### Instance methods
#### `config`
Definition: `config(options: L10nOptions): void`
Description: Used to configure certain settings of l10n, see [`L10nOptions`](#l10noptions) for more options.
Use case:
```ts
l10n.config({
// Used to display the default language with this value if no translation is found
fallbackLanguage: 'zh-Hans-CN',
// If you don't like the default value of LOCAL_STORAGE_LANGUAGE_KEY, you can change it here, but make sure it's before changeLanguage
localStorageLanguageKey: 'localization-editor/langauge'
})
```
---
#### `changeLanguage`
Definition: `changeLanguage(language: Intl.BCP47LanguageTag): void`
Description: Used to dynamically switch languages, see [``BCP47 Language Tag``](https://www.techonthenet.com/js/language_tags.php) for more information
Use case:
```ts
l10n.changeLanguage('zh-Hans-CN')
```
> ***Note: After calling this method, the game will be restarted automatically, please make sure to do the data persistence work***
---
#### `t`
Definition: `t(key: L10nKey, options?: StandardOption): L10nValue`
Description: Returns the L10nValue corresponding to the current language data according to the incoming L10nKey, see [`StandardOption`](#standardoption) for more options.
Use case:
```ts
console.log(l10n.t('this_is_apple'))
// This is an apple
```
> ***Note: The language data needs to be generated after compilation with the Localization Editor plugin.***
>
> ***Cannot use l10n.t in static initialization, e.g. `static name = l10n.t('xxx_name')`***
>
> ***Unable to scan when variable is used as parameter, e.g. `let name = 'this_is_apple'; l10n.t(name)`***
---
#### `exists`
Definition: `exists(key: L10nKey): boolean`
Description: Returns whether the key exists or not
Use case:
```ts
console.log(l10n.exists('test_key'))
```
---
#### `currentLanguage`
Definition: `get currentLanguage(): Intl.BCP47LanguageTag`
Description: Returns the [``BCP47 Language Tag``](https://www.techonthenet.com/js/language_tags.php) for the current language.
Use case:
```ts
console.log(l10n.currentLanguage)
// 'zh-Hans-CN'
```
---
#### `languages`
Definition: `get languages(): readonly Intl.BCP47LanguageTag[]`
Description: Returns an array of [``BCP47 Language Tag``](https://www.techonthenet.com/js/language_tags.php) for the currently available languages, which can be used as a data source for switching language dropdown boxes
Use case:
```ts
console.log(l10n.languages)
// ['zh-Hans-CN', 'en-US']
```
---
#### `direction`
Definition: `direction(language?: Intl. BCP47LanguageTag): TextInfoDirection`
Description: Most languages respect the left-to-right reading convention, but some languages are exceptions such as Arabic, this method learns the [TextInfoDirection](#textinfodirection) of the incoming language.
Use case:
```ts
console.log(l10n.direction('ar'))
// 'rtl'
```
---
#### `on`
Definition: `on(event: L10nListenEvent, callback: (. .args: any[]) => void)`
Description: Used to register [L10nListenEvent](#l10n) event callbacks for [l10n](#l10nlistenevent), such as `languageChanged`.
use case:
```ts
l10n.on(L10nListenEvent.languageChanged, (. .args: any[]) => {
// some actions after switching language, some data can be persisted here and the whole game scene will be restarted afterwards
})
```
--
#### `off`
Definition: `off(event: L10nListenEvent, callback: (.. .args: any[]) => void)`
Description: Callback for the [L10nListenEvent](#L10n) event used to counter-register [l10n](#L10nListenEvent)
> ***Make sure to make on and off appear in pairs to ensure proper destruction of useless data***
---
# Alias
| alias | original type |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `L10nKey` | `string` |
| `L10nValue` | `string` |
| `TextInfoDirection` | `'ltr' / 'rtl'` |
| `FallbackLanguage` | `string / readonly string[] / FallbackLanguageObjectList / ((language: Intl.BCP47LanguageTag) => string / readonly string[] / FallbackLanguageObjectList` |
---
# Interface
## `L10nOptions`
| function/variable name | type | optional |
|---------------------------|-------------------------------------|-----|
| `fallbackLanguage` | `false` / [`FallbackLanguage`](# alias) | yes |
| `localStorageLanguageKey` | `string` | yes |
| `beforeTranslate` | `(key: L10nKey) => L10nValue` | yes |
| `afterTranslate` | `(key: L10nKey) => L10nValue` | yes |
| `returnNull` | `boolean` | yes |
| `returnEmptyString` | `boolean` | yes |
---
## `ResourceList`
| function/variable name | type | optional |
|--------------------|---------------------------|-----|
| `defaultLanguage` | `Intl.BCP47LanguageTag` | yes |
| `fallbackLanguage` | `Intl.BCP47LanguageTag` | yes |
| `languages` | `Intl.BCP47LanguageTag[]` | no |
## `ResourceBundle`
| function/variable name | type | optional |
|-------------------------------------|---------------------------------|-----|
| `[language: Intl.BCP47LanguageTag]` | [`ResourceData`](#resourcedata) | no |
---
## `ResourceData`
| function/variable name | type | optional |
|-----------------------|---------------------------------|-----|
| `[namespace: string]` | [`ResourceItem`](#resourceitem) | no |
---
## `ResourceItem`
| function/variable name | type | optional |
|-----------------|-------|-----|
| `[key: string]` | `any` | no |
---
## `FallbackLanguageObjectList`
| function/variable name | type | optional |
|----------------------|---------------------|-----|
| `[language: string]` | `readonly string[]` | no |
---
## `StandardOption`
| function/variable name | type | optional |
|--------------------|---------------------------|-----|
| `count` | `number` | yes |
| `defaultValue` | `L10nValue` | yes |
| `language` | `Intl.BCP47LanguageTag` | yes |
| `fallbackLanguage` | [`FallbackLanguage`](# alias) | yes |
---
# Enumerations
## `L10nListenEvent`
| function/variable name | type |
|-------------------|-------------------|
| `languageChanged` | `languageChanged` |
| `onMissingKey` | `missingKey` |

View File

@@ -0,0 +1,351 @@
# Localization Editor API
## 快速开始
### [核心功能 `l10n`](#l10n)
l10n提供了核心翻译功能以及icu功能同时也提供的切换语言的功能。
我们会将切换后的目标语言存储于`localStorage`中,同时也会自动重启项目运行时,并在下次启动时读取`localStorage`配置以完成整个语言切换流程。
> ***因此我们希望用户在切换语言之前务必处理好数据持久化工作***
### 导入[`l10n`](#l10n)模块
localization-editor所提供的所有api都将从db://localization-editor/l10n进行具名导入
```typescript
import { l10n } from 'db://localization-editor/l10n'
```
### 使用翻译api
```typescript
// 任意component组件代码中
// l10n是localization的核心功能
import { l10n } from 'db://localization-editor/l10n'
import { _decorator, Label, Component } from 'cc';
@ccclass('SomeComponent')
class SomeComponent extends Component {
// ......
someMethod() {
// 将返回this_is_an_apple所对应文案
const text = l10n.t("this_is_an_apple")
}
// ......
}
```
### API详细说明
- 类:[`L10nManager`](#l10nmanager)
---
- 接口:[`ResourceList`](#resourcelist)
- 接口:[`ResourceBundle`](#resourcebundle)
- 接口:[`ResourceData`](#resourcedata)
- 接口:[`ResourceItem`](#resourceitem)
- 接口:[`FallbackLanguageObjectList`](#fallbacklanguageobjectlist)
- 接口:[`L10nOptions`](#l10noptions)
- 接口:[`StandardOption`](#standardoption)
---
- 枚举:[`L10nListenEvent`](#l10nlistenevent)
---
- 别名:[`L10nKey`](#别名)
- 别名:[`L10nValue`](#别名)
- 别名:[`TextInfoDirection`](#别名)
- 别名:[`FallbackLanguage`](#别名)
---
# `L10nManager`
导入示例:
```ts
import { L10nManager } from 'db://localization-editor/l10n'
```
描述:
通常我们不建议您自行使用或构造该类型。
而我们提供了[`l10n`](#l10n)作为全局单例以使用翻译功能。
---
## 索引
### 构造函数
- `L10nManager` **private**
---
### 全局变量
#### `l10n`
定义: `const l10n: L10nManager`
---
### 静态属性
#### `LOCAL_STORAGE_LANGUAGE_KEY`
定义: `static LOCAL_STORAGE_LANGUAGE_KEY: string`
描述: 当调用[`changeLanguage`](#changelanguage)切换游戏语言时,将使用`localStorage`
存储所切换的目标语言标记,并且使用[`LOCAL_STORAGE_LANGUAGE_KEY`](#localstoragelanguagekey)作为`localStorage`的key
备注:
| 默认值 | localization-editor/language |
|-----|------------------------------|
---
### 实例方法
#### `config`
定义: `config(options: L10nOptions): void`
描述: 用于配置l10n的某些设置探索更多选项可以查看[`L10nOptions`](#l10noptions)
用例:
```ts
l10n.config({
// 用于在默认语言没有找到相应翻译时,以该值进行补充显示
fallbackLanguage: 'zh-Hans-CN',
// 如果不喜欢LOCAL_STORAGE_LANGUAGE_KEY的默认值可以在此修改但是需要确保在changeLanguage之前
localStorageLanguageKey: 'localization-editor/langauge'
})
```
---
#### `changeLanguage`
定义: `changeLanguage(language: Intl.BCP47LanguageTag): void`
描述: 用于动态切换语言,请查看[`BCP47 Language Tag`](https://www.techonthenet.com/js/language_tags.php)以获得更多信息
用例:
```ts
l10n.changeLanguage('zh-Hans-CN')
```
> ***注意: 在调用此方法后,会自动重启游戏,请务必做好数据持久化工作***
---
#### `t`
定义: `t(key: L10nKey, options?: StandardOption): L10nValue`
描述: 根据传入的L10nKey返回当前语言数据中所对应的L10nValue探索更多选项可以查看[`StandardOption`](#standardoption)
用例:
```ts
console.log(l10n.t('this_is_apple'))
// 这是一个苹果
```
> ***注意: 语言数据需要配合Localization Editor插件在编译后生成。***
>
> ***无法在静态初始化中使用l10n.t 比如`static name = l10n.t('xxx_name')`***
>
> ***变量作为参数时无法扫描,比如`let name = 'this_is_apple'; l10n.t(name)`***
---
#### `exists`
定义: `exists(key: L10nKey): boolean`
描述: 返回是否存在key
用例:
```ts
console.log(l10n.exists('test_key'))
```
---
#### `currentLanguage`
定义: `get currentLanguage(): Intl.BCP47LanguageTag`
描述: 返回当前语言的[`BCP47 Language Tag`](https://www.techonthenet.com/js/language_tags.php)
用例:
```ts
console.log(l10n.currentLanguage)
// 'zh-Hans-CN'
```
---
#### `languages`
定义: `get languages(): readonly Intl.BCP47LanguageTag[]`
描述: 返回当前可用语言的[`BCP47 Language Tag`](https://www.techonthenet.com/js/language_tags.php)数组,可利用该方法作为切换语言下拉框的数据源
用例:
```ts
console.log(l10n.languages)
// ['zh-Hans-CN', 'en-US']
```
---
#### `direction`
定义: `direction(language?: Intl.BCP47LanguageTag): TextInfoDirection`
描述: 绝大多数语言都尊崇从左到右的阅读习惯,但某些语言却例外比如阿拉伯语,此方法可以得知所传入语言的[TextInfoDirection](#textinfodirection)
用例:
```ts
console.log(l10n.direction('ar'))
// 'rtl'
```
---
#### `on`
定义: `on(event: L10nListenEvent, callback: (...args: any[]) => void)`
描述: 用于注册[l10n](#l10n)的[L10nListenEvent](#l10nlistenevent)事件回调,比如`languageChanged`
用例:
```ts
l10n.on(L10nListenEvent.languageChanged, (...args: any[]) => {
//在切换语言后的一些操作,某些数据可以放在这里持久化,之后便会重启整个游戏场景
})
```
---
#### `off`
定义: `off(event: L10nListenEvent, callback: (...args: any[]) => void)`
描述: 用于反注册[l10n](#l10n)的[L10nListenEvent](#L10nListenEvent)事件回调
> ***请务必使on与off成对出现确保正确的销毁无用数据***
---
# 别名
| 别名 | 原类型 |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `L10nKey` | `string` |
| `L10nValue` | `string` |
| `TextInfoDirection` | `'ltr' / 'rtl'` |
| `FallbackLanguage` | `string / readonly string[] / FallbackLanguageObjectList / ((language: Intl.BCP47LanguageTag) => string / readonly string[] / FallbackLanguageObjectList` |
---
# 接口
## `L10nOptions`
| 函数/变量名 | 类型 | 可选 |
|---------------------------|-------------------------------------|-----|
| `fallbackLanguage` | `false` / [`FallbackLanguage`](#别名) | 是 |
| `localStorageLanguageKey` | `string` | 是 |
| `beforeTranslate` | `(key: L10nKey) => L10nValue` | 是 |
| `afterTranslate` | `(key: L10nKey) => L10nValue` | 是 |
| `returnNull` | `boolean` | 是 |
| `returnEmptyString` | `boolean` | 是 |
---
## `ResourceList`
| 函数/变量名 | 类型 | 可选 |
|--------------------|---------------------------|-----|
| `defaultLanguage` | `Intl.BCP47LanguageTag` | 是 |
| `fallbackLanguage` | `Intl.BCP47LanguageTag` | 是 |
| `languages` | `Intl.BCP47LanguageTag[]` | 否 |
## `ResourceBundle`
| 函数/变量名 | 类型 | 可选 |
|-------------------------------------|---------------------------------|-----|
| `[language: Intl.BCP47LanguageTag]` | [`ResourceData`](#resourcedata) | 否 |
---
## `ResourceData`
| 函数/变量名 | 类型 | 可选 |
|-----------------------|---------------------------------|-----|
| `[namespace: string]` | [`ResourceItem`](#resourceitem) | 否 |
---
## `ResourceItem`
| 函数/变量名 | 类型 | 可选 |
|-----------------|-------|-----|
| `[key: string]` | `any` | 否 |
---
## `FallbackLanguageObjectList`
| 函数/变量名 | 类型 | 可选 |
|----------------------|---------------------|-----|
| `[language: string]` | `readonly string[]` | 否 |
---
## `StandardOption`
| 函数/变量名 | 类型 | 可选 |
|--------------------|---------------------------|-----|
| `count` | `number` | 是 |
| `defaultValue` | `L10nValue` | 是 |
| `language` | `Intl.BCP47LanguageTag` | 是 |
| `fallbackLanguage` | [`FallbackLanguage`](#别名) | 是 |
---
# 枚举
## `L10nListenEvent`
| 函数/变量名 | 类型 |
|-------------------|-------------------|
| `languageChanged` | `languageChanged` |
| `onMissingKey` | `missingKey` |

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

View File

@@ -0,0 +1,979 @@
'use strict';
module.exports = {
open_panel: 'Translate',
'localization-editor': 'Localization Editor',
description: 'Localize editor',
service_provider: 'Translation service provider',
collection: 'Collect and count',
language_compilation: 'Compile language',
select_placeholder: 'Please select',
unselect_service_tip: 'If no translation service provider is selected, the automatic translation feature will not be supported',
local_language: 'Local development language:',
collected_from_resource_files: 'Collect from resource files',
required: '*required',
file_no_exist: 'File not exist',
cannot_empty: 'Cannot be empty',
common_languages: 'common languages',
cancel: 'Cancel',
confirm: 'Confirm',
ask_delete: 'Whether to delete it?',
localization_editor_project_config: 'Localization Editor Config For Project',
localization_editor_editor_config: 'Localization Editor Config For Editor',
localization_editor_provider_config: 'Localization Editor Translate Provider Configs',
loading_tips: 'loading...',
component: {
help: `https://docs.cocos.com/creator/manual/en/editor/l10n/l10n-label.html`,
string: 'Text within Label component',
count: 'The number to get a variant for',
key: 'Localized key',
},
YOUDAO: {
'zh-CHS': 'Chinese Simplified',
'zh-CHT': 'Traditional respectively',
},
label_inspector: {
no_origin: 'No original',
key: 'Key',
'key:': 'Key:',
save: 'Save',
reset: 'Reset',
saving: 'Saving...',
cannot_empty: 'Cannot be empty',
error_tooltip: 'Contains letters, numbers, underscores, hyphens, @, /, +, | \N cannot be any other character',
exist_media_tooltip: 'This key is the key of media',
edit: 'Edit',
},
build: {
compress_warning: 'Building failed because the picture with uuid "${a}" and the picture with uuid "${b}" have different settings for Compress Texture. Please keep Compress Texture settings for these pictures consistent.',
packable_warning: 'Translation failed for the picture with uuid "${uuid}", because it has option Dynamic Atlas turned on.',
use_polyfill: ' Polyfill script used',
use_language: 'Language used',
select_all: 'Select all',
default_language: 'Default language',
fallback_language: 'Alternate language',
},
home: {
po_name: 'GNU gettext PO File',
csv_name: 'Comma-Separated Values',
xlsx_name: 'Excel File',
service_provider: 'Service provider',
collection: 'Collection',
language_compilation: 'Compile language',
select_placeholder: 'Select',
unselect_service_tip: 'If no translation service provider is selected, the automatic translation function will not be supported',
'local_language:': 'Local development language:',
collected_from_resource_files: 'Collect from resource files',
required: '*Required',
delete: 'Delete',
preview: 'Preview',
combine: 'Combine',
translate: 'Translate',
complement: 'Complement',
not_recorded: 'notRecorded',
collect_and_count: 'Collect and count',
add_new_language: 'Add new language',
add: 'Add',
select: 'Select',
language: 'Language',
'language:': 'Language:',
extname: 'Extension name',
exclude_path: 'Exclude path',
language_for_service_provider: 'Language recognized by the service provider',
dir: 'Directory',
search_dir: 'Search directory',
translate_process: 'Translation progress',
combine_process: 'Compilation progress',
operation: 'Operation',
local_language: 'Local development language',
combine_tooltip: ' Text is calculated by the actual number of words, and resources are calculated by unit of 1',
translate_tooltip: 'Any entry is calculated in unit 1',
count: 'Count:',
collecting: 'Counting',
unselect: 'No development language selected',
save: 'Save',
complete: 'Complete',
collect_group: 'Collection Group',
turn_on_tip: 'Please enable Localization Editor tool',
turn_on: 'Enable Localization Editor',
turn_off_warning: 'After closing, all translation data and configurations will be cleared. Are you sure you still want to close?',
turn_off: 'Turn off L10N',
delete_data: 'Clear data',
delete_data_warning: 'The translated data will be permanently lost after clearing. Are you sure you want to clear the data?',
export: 'Export',
export_all: 'Export All',
},
translate: {
source_string_placeholder: 'Please enter what will be replaced at the URL of the asset',
dist_string_placeholder: 'Please enter the replaced content at the URL of the asset',
source_string: 'Search Content:',
dist_string: 'Replace Content:',
new_value: 'New Value',
old_value: 'Origin Value',
import_po: 'Import PO File',
export_po: 'Export PO File',
save: 'Save',
unfilled: 'Unfilled',
untranslated: 'Untranslated',
filled: 'Filled',
count: 'Count:',
translate: 'Translate',
translated: 'Translated',
key: 'Key',
'key:': 'Key:',
jump: 'skip',
origin: 'Original',
target: 'Translation',
import_all: 'Intelligent matching',
import: 'Import',
import_file: 'Import File',
'position:': 'Position:',
reference_uuid: 'Reference node\'s uuid:',
variant: 'Variant',
after_variant: 'After variant applied',
standard: 'Standard',
delete_variant: 'Delete variant',
cancel: 'Cancel',
confirm: 'Confirm',
cover: 'Cover',
conflict_dialog_title: '注意:',
conflict_dialog_content: 'The following data already exists in [translated], and the original data will disappear after overwriting. Are you sure you want to overwrite?',
unsaved_warning: 'Does the current translation need to be saved?',
delete_warning: ' are you sure to delete?',
auto_import_warning: '{length} files will be matched intelligently.{localLocale} (source language) will be replaced with {targetLocale} (target language)after matching. Are you sure to perform this operation?',
quit_warning: 'There are unsaved contents, are you sure to exit?',
import_tab_title: 'Imported Files',
import_file_conflicts_with_file_warning: 'Unable to import {num} data entries due to key conflicts, how would you like to resolve?',
saving_tips: 'Ongoing...',
},
language_code: {
'af': 'Afrikaans',
'af-ZA': 'Afrikaans (South Africa)',
'af-NA': 'Afrikaans (Namibia)',
'agq': 'Aghem',
'agq-CM': 'Aghem (Cameroon)',
'ak': 'Akan',
'ak-GH': 'Akan (Ghana)',
'am': 'Amharic',
'am-ET': 'Amharic (Ethiopia)',
'ar': 'Arabic',
'ar-YE': 'Arabic (Yemen)',
'ar-TN': 'Arabic (Tunisia)',
'ar-TD': 'Arabic (Chad)',
'ar-SY': 'Arabic (Syria)',
'ar-SS': 'Arabic (South Sudan)',
'ar-SO': 'Arabic (Somalia)',
'ar-SD': 'Arabic (Sudan)',
'ar-SA': 'Arabic (Saudi Arabia)',
'ar-QA': 'Arabic (Qatar)',
'ar-PS': 'Arabic (Palestinian Territories)',
'ar-OM': 'Arabic (Oman)',
'ar-MR': 'Arabic (Mauritania)',
'ar-MA': 'Arabic (Morocco)',
'ar-LY': 'Arabic (Libya)',
'ar-LB': 'Arabic (Lebanon)',
'ar-KW': 'Arabic (Kuwait)',
'ar-KM': 'Arabic (Comoros)',
'ar-JO': 'Arabic (Jordan)',
'ar-IQ': 'Arabic (Iraq)',
'ar-IL': 'Arabic (Israel)',
'ar-ER': 'Arabic (Eritrea)',
'ar-EH': 'Arabic (Western Sahara)',
'ar-EG': 'Arabic (Egypt)',
'ar-DZ': 'Arabic (Algeria)',
'ar-DJ': 'Arabic (Djibouti)',
'ar-BH': 'Arabic (Bahrain)',
'ar-AE': 'Arabic (United Arab Emirates)',
'ar-001': 'Modern Standard Arabic',
'as': 'Assamese',
'as-IN': 'Assamese (India)',
'asa': 'Asu',
'asa-TZ': 'Asu (Tanzania)',
'ast': 'Asturian',
'ast-ES': 'Asturian (Spain)',
'az': 'Azerbaijani',
'az-Latn-AZ': 'Azerbaijani (Latin, Azerbaijan)',
'az-Latn': 'Azerbaijani (Latin)',
'az-Cyrl-AZ': 'Azerbaijani (Cyrillic, Azerbaijan)',
'az-Cyrl': 'Azerbaijani (Cyrillic)',
'bas': 'Basaa',
'bas-CM': 'Basaa (Cameroon)',
'be': 'Belarusian',
'be-TARASK': 'Belarusian (Taraskievica orthography)',
'be-BY': 'Belarusian (Belarus)',
'bem': 'Bemba',
'bem-ZM': 'Bemba (Zambia)',
'bez': 'Bena',
'bez-TZ': 'Bena (Tanzania)',
'bg': 'Bulgarian',
'bg-BG': 'Bulgarian (Bulgaria)',
'bm': 'Bambara',
'bm-ML': 'Bambara (Mali)',
'bn': 'Bangla',
'bn-IN': 'Bangla (India)',
'bn-BD': 'Bangla (Bangladesh)',
'br': 'Breton',
'br-FR': 'Breton (France)',
'brx': 'Bodo',
'brx-IN': 'Bodo (India)',
'bs': 'Bosnian',
'bs-Latn-BA': 'Bosnian (Latin, Bosnia & Herzegovina)',
'bs-Latn': 'Bosnian (Latin)',
'bs-Cyrl-BA': 'Bosnian (Cyrillic, Bosnia & Herzegovina)',
'bs-Cyrl': 'Bosnian (Cyrillic)',
'ca': 'Catalan',
'ca-IT': 'Catalan (Italy)',
'ca-FR': 'Catalan (France)',
'ca-ES-VALENCIA': 'Catalan (Spain, Valencian)',
'ca-ES': 'Catalan (Spain)',
'ca-AD': 'Catalan (Andorra)',
'ccp': 'Chakma',
'ccp-IN': 'Chakma (India)',
'ccp-BD': 'Chakma (Bangladesh)',
'ce': 'Chechen',
'ce-RU': 'Chechen (Russia)',
'ceb': 'Cebuano',
'ceb-PH': 'Cebuano (Philippines)',
'cgg': 'Chiga',
'cgg-UG': 'Chiga (Uganda)',
'chr': 'Cherokee',
'chr-US': 'Cherokee (United States)',
'ckb': 'Central Kurdish',
'ckb-IR': 'Central Kurdish (Iran)',
'ckb-IQ': 'Central Kurdish (Iraq)',
'cs': 'Czech',
'cs-CZ': 'Czech (Czechia)',
'cy': 'Welsh',
'cy-GB': 'Welsh (United Kingdom)',
'da': 'Danish',
'da-GL': 'Danish (Greenland)',
'da-DK': 'Danish (Denmark)',
'dav': 'Taita',
'dav-KE': 'Taita (Kenya)',
'de': 'German',
'de-LU': 'German (Luxembourg)',
'de-LI': 'German (Liechtenstein)',
'de-IT': 'German (Italy)',
'de-DE': 'German (Germany)',
'de-CH': 'Swiss High German',
'de-BE': 'German (Belgium)',
'de-AT': 'Austrian German',
'dje': 'Zarma',
'dje-NE': 'Zarma (Niger)',
'doi': 'Dogri',
'doi-IN': 'Dogri (India)',
'dsb': 'Lower Sorbian',
'dsb-DE': 'Lower Sorbian (Germany)',
'dua': 'Duala',
'dua-CM': 'Duala (Cameroon)',
'dyo': 'Jola-Fonyi',
'dyo-SN': 'Jola-Fonyi (Senegal)',
'dz': 'Dzongkha',
'dz-BT': 'Dzongkha (Bhutan)',
'ebu': 'Embu',
'ebu-KE': 'Embu (Kenya)',
'ee': 'Ewe',
'ee-TG': 'Ewe (Togo)',
'ee-GH': 'Ewe (Ghana)',
'el': 'Greek',
'el-GR': 'Greek (Greece)',
'el-CY': 'Greek (Cyprus)',
'en': 'English',
'en-ZW': 'English (Zimbabwe)',
'en-ZM': 'English (Zambia)',
'en-ZA': 'English (South Africa)',
'en-WS': 'English (Samoa)',
'en-VU': 'English (Vanuatu)',
'en-VI': 'English (U.S. Virgin Islands)',
'en-VG': 'English (British Virgin Islands)',
'en-VC': 'English (St. Vincent & Grenadines)',
'en-US-POSIX': 'American English (Computer)',
'en-US': 'American English',
'en-UM': 'English (U.S. Outlying Islands)',
'en-UG': 'English (Uganda)',
'en-TZ': 'English (Tanzania)',
'en-TV': 'English (Tuvalu)',
'en-TT': 'English (Trinidad & Tobago)',
'en-TO': 'English (Tonga)',
'en-TK': 'English (Tokelau)',
'en-TC': 'English (Turks & Caicos Islands)',
'en-SZ': 'English (Eswatini)',
'en-SX': 'English (Sint Maarten)',
'en-SS': 'English (South Sudan)',
'en-SL': 'English (Sierra Leone)',
'en-SI': 'English (Slovenia)',
'en-SH': 'English (St. Helena)',
'en-SG': 'English (Singapore)',
'en-SE': 'English (Sweden)',
'en-SD': 'English (Sudan)',
'en-SC': 'English (Seychelles)',
'en-SB': 'English (Solomon Islands)',
'en-RW': 'English (Rwanda)',
'en-PW': 'English (Palau)',
'en-PR': 'English (Puerto Rico)',
'en-PN': 'English (Pitcairn Islands)',
'en-PK': 'English (Pakistan)',
'en-PH': 'English (Philippines)',
'en-PG': 'English (Papua New Guinea)',
'en-NZ': 'English (New Zealand)',
'en-NU': 'English (Niue)',
'en-NR': 'English (Nauru)',
'en-NL': 'English (Netherlands)',
'en-NG': 'English (Nigeria)',
'en-NF': 'English (Norfolk Island)',
'en-NA': 'English (Namibia)',
'en-MY': 'English (Malaysia)',
'en-MW': 'English (Malawi)',
'en-MV': 'English (Maldives)',
'en-MU': 'English (Mauritius)',
'en-MT': 'English (Malta)',
'en-MS': 'English (Montserrat)',
'en-MP': 'English (Northern Mariana Islands)',
'en-MO': 'English (Macao SAR China)',
'en-MH': 'English (Marshall Islands)',
'en-MG': 'English (Madagascar)',
'en-LS': 'English (Lesotho)',
'en-LR': 'English (Liberia)',
'en-LC': 'English (St. Lucia)',
'en-KY': 'English (Cayman Islands)',
'en-KN': 'English (St. Kitts & Nevis)',
'en-KI': 'English (Kiribati)',
'en-KE': 'English (Kenya)',
'en-JM': 'English (Jamaica)',
'en-JE': 'English (Jersey)',
'en-IO': 'English (British Indian Ocean Territory)',
'en-IN': 'English (India)',
'en-IM': 'English (Isle of Man)',
'en-IL': 'English (Israel)',
'en-IE': 'English (Ireland)',
'en-HK': 'English (Hong Kong SAR China)',
'en-GY': 'English (Guyana)',
'en-GU': 'English (Guam)',
'en-GM': 'English (Gambia)',
'en-GI': 'English (Gibraltar)',
'en-GH': 'English (Ghana)',
'en-GG': 'English (Guernsey)',
'en-GD': 'English (Grenada)',
'en-GB': 'British English',
'en-FM': 'English (Micronesia)',
'en-FK': 'English (Falkland Islands)',
'en-FJ': 'English (Fiji)',
'en-FI': 'English (Finland)',
'en-ER': 'English (Eritrea)',
'en-DM': 'English (Dominica)',
'en-DK': 'English (Denmark)',
'en-DG': 'English (Diego Garcia)',
'en-DE': 'English (Germany)',
'en-CY': 'English (Cyprus)',
'en-CX': 'English (Christmas Island)',
'en-CM': 'English (Cameroon)',
'en-CK': 'English (Cook Islands)',
'en-CH': 'English (Switzerland)',
'en-CC': 'English (Cocos [Keeling] Islands)',
'en-CA': 'Canadian English',
'en-BZ': 'English (Belize)',
'en-BW': 'English (Botswana)',
'en-BS': 'English (Bahamas)',
'en-BM': 'English (Bermuda)',
'en-BI': 'English (Burundi)',
'en-BE': 'English (Belgium)',
'en-BB': 'English (Barbados)',
'en-AU': 'Australian English',
'en-AT': 'English (Austria)',
'en-AS': 'English (American Samoa)',
'en-AI': 'English (Anguilla)',
'en-AG': 'English (Antigua & Barbuda)',
'en-AE': 'English (United Arab Emirates)',
'en-150': 'English (Europe)',
'en-001': 'English (world)',
'eo': 'Esperanto',
'eo-001': 'Esperanto (world)',
'es': 'Spanish',
'es-VE': 'Spanish (Venezuela)',
'es-UY': 'Spanish (Uruguay)',
'es-US': 'Spanish (United States)',
'es-SV': 'Spanish (El Salvador)',
'es-PY': 'Spanish (Paraguay)',
'es-PR': 'Spanish (Puerto Rico)',
'es-PH': 'Spanish (Philippines)',
'es-PE': 'Spanish (Peru)',
'es-PA': 'Spanish (Panama)',
'es-NI': 'Spanish (Nicaragua)',
'es-MX': 'Mexican Spanish',
'es-IC': 'Spanish (Canary Islands)',
'es-HN': 'Spanish (Honduras)',
'es-GT': 'Spanish (Guatemala)',
'es-GQ': 'Spanish (Equatorial Guinea)',
'es-ES': 'European Spanish',
'es-EC': 'Spanish (Ecuador)',
'es-EA': 'Spanish (Ceuta & Melilla)',
'es-DO': 'Spanish (Dominican Republic)',
'es-CU': 'Spanish (Cuba)',
'es-CR': 'Spanish (Costa Rica)',
'es-CO': 'Spanish (Colombia)',
'es-CL': 'Spanish (Chile)',
'es-BZ': 'Spanish (Belize)',
'es-BR': 'Spanish (Brazil)',
'es-BO': 'Spanish (Bolivia)',
'es-AR': 'Spanish (Argentina)',
'es-419': 'Latin American Spanish',
'et': 'Estonian',
'et-EE': 'Estonian (Estonia)',
'eu': 'Basque',
'eu-ES': 'Basque (Spain)',
'ewo': 'Ewondo',
'ewo-CM': 'Ewondo (Cameroon)',
'fa': 'Persian',
'fa-IR': 'Persian (Iran)',
'fa-AF': 'Dari',
'ff': 'Fulah',
'ff-Latn-SN': 'Fulah (Latin, Senegal)',
'ff-Latn-SL': 'Fulah (Latin, Sierra Leone)',
'ff-Latn-NG': 'Fulah (Latin, Nigeria)',
'ff-Latn-NE': 'Fulah (Latin, Niger)',
'ff-Latn-MR': 'Fulah (Latin, Mauritania)',
'ff-Latn-LR': 'Fulah (Latin, Liberia)',
'ff-Latn-GW': 'Fulah (Latin, Guinea-Bissau)',
'ff-Latn-GN': 'Fulah (Latin, Guinea)',
'ff-Latn-GM': 'Fulah (Latin, Gambia)',
'ff-Latn-GH': 'Fulah (Latin, Ghana)',
'ff-Latn-CM': 'Fulah (Latin, Cameroon)',
'ff-Latn-BF': 'Fulah (Latin, Burkina Faso)',
'ff-Latn': 'Fulah (Latin)',
'ff-Adlm-SN': 'Fulah (Adlam, Senegal)',
'ff-Adlm-SL': 'Fulah (Adlam, Sierra Leone)',
'ff-Adlm-NG': 'Fulah (Adlam, Nigeria)',
'ff-Adlm-NE': 'Fulah (Adlam, Niger)',
'ff-Adlm-MR': 'Fulah (Adlam, Mauritania)',
'ff-Adlm-LR': 'Fulah (Adlam, Liberia)',
'ff-Adlm-GW': 'Fulah (Adlam, Guinea-Bissau)',
'ff-Adlm-GN': 'Fulah (Adlam, Guinea)',
'ff-Adlm-GM': 'Fulah (Adlam, Gambia)',
'ff-Adlm-GH': 'Fulah (Adlam, Ghana)',
'ff-Adlm-CM': 'Fulah (Adlam, Cameroon)',
'ff-Adlm-BF': 'Fulah (Adlam, Burkina Faso)',
'ff-Adlm': 'Fulah (Adlam)',
'fi': 'Finnish',
'fi-FI': 'Finnish (Finland)',
'fil': 'Filipino',
'fil-PH': 'Filipino (Philippines)',
'fo': 'Faroese',
'fo-FO': 'Faroese (Faroe Islands)',
'fo-DK': 'Faroese (Denmark)',
'fr': 'French',
'fr-YT': 'French (Mayotte)',
'fr-WF': 'French (Wallis & Futuna)',
'fr-VU': 'French (Vanuatu)',
'fr-TN': 'French (Tunisia)',
'fr-TG': 'French (Togo)',
'fr-TD': 'French (Chad)',
'fr-SY': 'French (Syria)',
'fr-SN': 'French (Senegal)',
'fr-SC': 'French (Seychelles)',
'fr-RW': 'French (Rwanda)',
'fr-RE': 'French (Réunion)',
'fr-PM': 'French (St. Pierre & Miquelon)',
'fr-PF': 'French (French Polynesia)',
'fr-NE': 'French (Niger)',
'fr-NC': 'French (New Caledonia)',
'fr-MU': 'French (Mauritius)',
'fr-MR': 'French (Mauritania)',
'fr-MQ': 'French (Martinique)',
'fr-ML': 'French (Mali)',
'fr-MG': 'French (Madagascar)',
'fr-MF': 'French (St. Martin)',
'fr-MC': 'French (Monaco)',
'fr-MA': 'French (Morocco)',
'fr-LU': 'French (Luxembourg)',
'fr-KM': 'French (Comoros)',
'fr-HT': 'French (Haiti)',
'fr-GQ': 'French (Equatorial Guinea)',
'fr-GP': 'French (Guadeloupe)',
'fr-GN': 'French (Guinea)',
'fr-GF': 'French (French Guiana)',
'fr-GA': 'French (Gabon)',
'fr-FR': 'French (France)',
'fr-DZ': 'French (Algeria)',
'fr-DJ': 'French (Djibouti)',
'fr-CM': 'French (Cameroon)',
'fr-CI': 'French (Côte dIvoire)',
'fr-CH': 'Swiss French',
'fr-CG': 'French (Congo - Brazzaville)',
'fr-CF': 'French (Central African Republic)',
'fr-CD': 'French (Congo - Kinshasa)',
'fr-CA': 'Canadian French',
'fr-BL': 'French (St. Barthélemy)',
'fr-BJ': 'French (Benin)',
'fr-BI': 'French (Burundi)',
'fr-BF': 'French (Burkina Faso)',
'fr-BE': 'French (Belgium)',
'fur': 'Friulian',
'fur-IT': 'Friulian (Italy)',
'fy': 'Western Frisian',
'fy-NL': 'Western Frisian (Netherlands)',
'ga': 'Irish',
'ga-IE': 'Irish (Ireland)',
'ga-GB': 'Irish (United Kingdom)',
'gd': 'Scottish Gaelic',
'gd-GB': 'Scottish Gaelic (United Kingdom)',
'gl': 'Galician',
'gl-ES': 'Galician (Spain)',
'gsw': 'Swiss German',
'gsw-LI': 'Swiss German (Liechtenstein)',
'gsw-FR': 'Swiss German (France)',
'gsw-CH': 'Swiss German (Switzerland)',
'gu': 'Gujarati',
'gu-IN': 'Gujarati (India)',
'guz': 'Gusii',
'guz-KE': 'Gusii (Kenya)',
'gv': 'Manx',
'gv-IM': 'Manx (Isle of Man)',
'ha': 'Hausa',
'ha-NG': 'Hausa (Nigeria)',
'ha-NE': 'Hausa (Niger)',
'ha-GH': 'Hausa (Ghana)',
'haw': 'Hawaiian',
'haw-US': 'Hawaiian (United States)',
'he': 'Hebrew',
'he-IL': 'Hebrew (Israel)',
'hi': 'Hindi',
'hi-Latn-IN': 'Hindi (Latin, India)',
'hi-Latn': 'Hindi (Latin)',
'hi-IN': 'Hindi (India)',
'hr': 'Croatian',
'hr-HR': 'Croatian (Croatia)',
'hr-BA': 'Croatian (Bosnia & Herzegovina)',
'hsb': 'Upper Sorbian',
'hsb-DE': 'Upper Sorbian (Germany)',
'hu': 'Hungarian',
'hu-HU': 'Hungarian (Hungary)',
'hy': 'Armenian',
'hy-AM': 'Armenian (Armenia)',
'ia': 'Interlingua',
'ia-001': 'Interlingua (world)',
'id': 'Indonesian',
'id-ID': 'Indonesian (Indonesia)',
'ig': 'Igbo',
'ig-NG': 'Igbo (Nigeria)',
'is': 'Icelandic',
'is-IS': 'Icelandic (Iceland)',
'it': 'Italian',
'it-VA': 'Italian (Vatican City)',
'it-SM': 'Italian (San Marino)',
'it-IT': 'Italian (Italy)',
'it-CH': 'Italian (Switzerland)',
'ja': 'Japanese',
'ja-JP': 'Japanese (Japan)',
'jgo': 'Ngomba',
'jgo-CM': 'Ngomba (Cameroon)',
'jmc': 'Machame',
'jmc-TZ': 'Machame (Tanzania)',
'jv': 'Javanese',
'jv-ID': 'Javanese (Indonesia)',
'ka': 'Georgian',
'ka-GE': 'Georgian (Georgia)',
'kab': 'Kabyle',
'kab-DZ': 'Kabyle (Algeria)',
'kam': 'Kamba',
'kam-KE': 'Kamba (Kenya)',
'kde': 'Makonde',
'kde-TZ': 'Makonde (Tanzania)',
'kea': 'Kabuverdianu',
'kea-CV': 'Kabuverdianu (Cape Verde)',
'kgp': 'Kaingang',
'kgp-BR': 'Kaingang (Brazil)',
'khq': 'Koyra Chiini',
'khq-ML': 'Koyra Chiini (Mali)',
'ki': 'Kikuyu',
'ki-KE': 'Kikuyu (Kenya)',
'kk': 'Kazakh',
'kk-KZ': 'Kazakh (Kazakhstan)',
'kkj': 'Kako',
'kkj-CM': 'Kako (Cameroon)',
'kl': 'Kalaallisut',
'kl-GL': 'Kalaallisut (Greenland)',
'kln': 'Kalenjin',
'kln-KE': 'Kalenjin (Kenya)',
'km': 'Khmer',
'km-KH': 'Khmer (Cambodia)',
'kn': 'Kannada',
'kn-IN': 'Kannada (India)',
'ko': 'Korean',
'ko-KR': 'Korean (South Korea)',
'ko-KP': 'Korean (North Korea)',
'kok': 'Konkani',
'kok-IN': 'Konkani (India)',
'ks': 'Kashmiri',
'ks-Deva-IN': 'Kashmiri (Devanagari, India)',
'ks-Deva': 'Kashmiri (Devanagari)',
'ks-Arab-IN': 'Kashmiri (Arabic, India)',
'ks-Arab': 'Kashmiri (Arabic)',
'ksb': 'Shambala',
'ksb-TZ': 'Shambala (Tanzania)',
'ksf': 'Bafia',
'ksf-CM': 'Bafia (Cameroon)',
'ksh': 'Colognian',
'ksh-DE': 'Colognian (Germany)',
'ku': 'Kurdish',
'ku-TR': 'Kurdish (Turkey)',
'kw': 'Cornish',
'kw-GB': 'Cornish (United Kingdom)',
'ky': 'Kyrgyz',
'ky-KG': 'Kyrgyz (Kyrgyzstan)',
'lag': 'Langi',
'lag-TZ': 'Langi (Tanzania)',
'lb': 'Luxembourgish',
'lb-LU': 'Luxembourgish (Luxembourg)',
'lg': 'Ganda',
'lg-UG': 'Ganda (Uganda)',
'lkt': 'Lakota',
'lkt-US': 'Lakota (United States)',
'ln': 'Lingala',
'ln-CG': 'Lingala (Congo - Brazzaville)',
'ln-CF': 'Lingala (Central African Republic)',
'ln-CD': 'Lingala (Congo - Kinshasa)',
'ln-AO': 'Lingala (Angola)',
'lo': 'Lao',
'lo-LA': 'Lao (Laos)',
'lrc': 'Northern Luri',
'lrc-IR': 'Northern Luri (Iran)',
'lrc-IQ': 'Northern Luri (Iraq)',
'lt': 'Lithuanian',
'lt-LT': 'Lithuanian (Lithuania)',
'lu': 'Luba-Katanga',
'lu-CD': 'Luba-Katanga (Congo - Kinshasa)',
'luo': 'Luo',
'luo-KE': 'Luo (Kenya)',
'luy': 'Luyia',
'luy-KE': 'Luyia (Kenya)',
'lv': 'Latvian',
'lv-LV': 'Latvian (Latvia)',
'mai': 'Maithili',
'mai-IN': 'Maithili (India)',
'mas': 'Masai',
'mas-TZ': 'Masai (Tanzania)',
'mas-KE': 'Masai (Kenya)',
'mer': 'Meru',
'mer-KE': 'Meru (Kenya)',
'mfe': 'Morisyen',
'mfe-MU': 'Morisyen (Mauritius)',
'mg': 'Malagasy',
'mg-MG': 'Malagasy (Madagascar)',
'mgh': 'Makhuwa-Meetto',
'mgh-MZ': 'Makhuwa-Meetto (Mozambique)',
'mgo': 'Metaʼ',
'mgo-CM': 'Metaʼ (Cameroon)',
'mi': 'Māori',
'mi-NZ': 'Māori (New Zealand)',
'mk': 'Macedonian',
'mk-MK': 'Macedonian (North Macedonia)',
'ml': 'Malayalam',
'ml-IN': 'Malayalam (India)',
'mn': 'Mongolian',
'mn-MN': 'Mongolian (Mongolia)',
'mni': 'Manipuri',
'mni-Beng-IN': 'Manipuri (Bangla, India)',
'mni-Beng': 'Manipuri (Bangla)',
'mr': 'Marathi',
'mr-IN': 'Marathi (India)',
'ms': 'Malay',
'ms-SG': 'Malay (Singapore)',
'ms-MY': 'Malay (Malaysia)',
'ms-ID': 'Malay (Indonesia)',
'ms-BN': 'Malay (Brunei)',
'mt': 'Maltese',
'mt-MT': 'Maltese (Malta)',
'mua': 'Mundang',
'mua-CM': 'Mundang (Cameroon)',
'my': 'Burmese',
'my-MM': 'Burmese (Myanmar [Burma])',
'mzn': 'Mazanderani',
'mzn-IR': 'Mazanderani (Iran)',
'naq': 'Nama',
'naq-NA': 'Nama (Namibia)',
'nb': 'Norwegian Bokmål',
'nb-SJ': 'Norwegian Bokmål (Svalbard & Jan Mayen)',
'nb-NO': 'Norwegian Bokmål (Norway)',
'nd': 'North Ndebele',
'nd-ZW': 'North Ndebele (Zimbabwe)',
'nds': 'Low German',
'nds-NL': 'Low Saxon',
'nds-DE': 'Low German (Germany)',
'ne': 'Nepali',
'ne-NP': 'Nepali (Nepal)',
'ne-IN': 'Nepali (India)',
'nl': 'Dutch',
'nl-SX': 'Dutch (Sint Maarten)',
'nl-SR': 'Dutch (Suriname)',
'nl-NL': 'Dutch (Netherlands)',
'nl-CW': 'Dutch (Curaçao)',
'nl-BQ': 'Dutch (Caribbean Netherlands)',
'nl-BE': 'Flemish',
'nl-AW': 'Dutch (Aruba)',
'nmg': 'Kwasio',
'nmg-CM': 'Kwasio (Cameroon)',
'nn': 'Norwegian Nynorsk',
'nn-NO': 'Norwegian Nynorsk (Norway)',
'nnh': 'Ngiemboon',
'nnh-CM': 'Ngiemboon (Cameroon)',
'no': 'Norwegian',
'nus': 'Nuer',
'nus-SS': 'Nuer (South Sudan)',
'nyn': 'Nyankole',
'nyn-UG': 'Nyankole (Uganda)',
'om': 'Oromo',
'om-KE': 'Oromo (Kenya)',
'om-ET': 'Oromo (Ethiopia)',
'or': 'Odia',
'or-IN': 'Odia (India)',
'os': 'Ossetic',
'os-RU': 'Ossetic (Russia)',
'os-GE': 'Ossetic (Georgia)',
'pa': 'Punjabi',
'pa-Guru-IN': 'Punjabi (Gurmukhi, India)',
'pa-Guru': 'Punjabi (Gurmukhi)',
'pa-Arab-PK': 'Punjabi (Arabic, Pakistan)',
'pa-Arab': 'Punjabi (Arabic)',
'pcm': 'Nigerian Pidgin',
'pcm-NG': 'Nigerian Pidgin (Nigeria)',
'pl': 'Polish',
'pl-PL': 'Polish (Poland)',
'ps': 'Pashto',
'ps-PK': 'Pashto (Pakistan)',
'ps-AF': 'Pashto (Afghanistan)',
'pt': 'Portuguese',
'pt-TL': 'Portuguese (Timor-Leste)',
'pt-ST': 'Portuguese (São Tomé & Príncipe)',
'pt-PT': 'European Portuguese',
'pt-MZ': 'Portuguese (Mozambique)',
'pt-MO': 'Portuguese (Macao SAR China)',
'pt-LU': 'Portuguese (Luxembourg)',
'pt-GW': 'Portuguese (Guinea-Bissau)',
'pt-GQ': 'Portuguese (Equatorial Guinea)',
'pt-CV': 'Portuguese (Cape Verde)',
'pt-CH': 'Portuguese (Switzerland)',
'pt-BR': 'Brazilian Portuguese',
'pt-AO': 'Portuguese (Angola)',
'qu': 'Quechua',
'qu-PE': 'Quechua (Peru)',
'qu-EC': 'Quechua (Ecuador)',
'qu-BO': 'Quechua (Bolivia)',
'rm': 'Romansh',
'rm-CH': 'Romansh (Switzerland)',
'rn': 'Rundi',
'rn-BI': 'Rundi (Burundi)',
'ro': 'Romanian',
'ro-RO': 'Romanian (Romania)',
'ro-MD': 'Moldavian',
'rof': 'Rombo',
'rof-TZ': 'Rombo (Tanzania)',
'ru': 'Russian',
'ru-UA': 'Russian (Ukraine)',
'ru-RU': 'Russian (Russia)',
'ru-MD': 'Russian (Moldova)',
'ru-KZ': 'Russian (Kazakhstan)',
'ru-KG': 'Russian (Kyrgyzstan)',
'ru-BY': 'Russian (Belarus)',
'rw': 'Kinyarwanda',
'rw-RW': 'Kinyarwanda (Rwanda)',
'rwk': 'Rwa',
'rwk-TZ': 'Rwa (Tanzania)',
'sa': 'Sanskrit',
'sa-IN': 'Sanskrit (India)',
'sah': 'Sakha',
'sah-RU': 'Sakha (Russia)',
'saq': 'Samburu',
'saq-KE': 'Samburu (Kenya)',
'sat': 'Santali',
'sat-Olck-IN': 'Santali (Ol Chiki, India)',
'sat-Olck': 'Santali (Ol Chiki)',
'sbp': 'Sangu',
'sbp-TZ': 'Sangu (Tanzania)',
'sc': 'Sardinian',
'sc-IT': 'Sardinian (Italy)',
'sd': 'Sindhi',
'sd-Deva-IN': 'Sindhi (Devanagari, India)',
'sd-Deva': 'Sindhi (Devanagari)',
'sd-Arab-PK': 'Sindhi (Arabic, Pakistan)',
'sd-Arab': 'Sindhi (Arabic)',
'se': 'Northern Sami',
'se-SE': 'Northern Sami (Sweden)',
'se-NO': 'Northern Sami (Norway)',
'se-FI': 'Northern Sami (Finland)',
'seh': 'Sena',
'seh-MZ': 'Sena (Mozambique)',
'ses': 'Koyraboro Senni',
'ses-ML': 'Koyraboro Senni (Mali)',
'sg': 'Sango',
'sg-CF': 'Sango (Central African Republic)',
'shi': 'Tachelhit',
'shi-Tfng-MA': 'Tachelhit (Tifinagh, Morocco)',
'shi-Tfng': 'Tachelhit (Tifinagh)',
'shi-Latn-MA': 'Tachelhit (Latin, Morocco)',
'shi-Latn': 'Tachelhit (Latin)',
'si': 'Sinhala',
'si-LK': 'Sinhala (Sri Lanka)',
'sk': 'Slovak',
'sk-SK': 'Slovak (Slovakia)',
'sl': 'Slovenian',
'sl-SI': 'Slovenian (Slovenia)',
'smn': 'Inari Sami',
'smn-FI': 'Inari Sami (Finland)',
'sn': 'Shona',
'sn-ZW': 'Shona (Zimbabwe)',
'so': 'Somali',
'so-SO': 'Somali (Somalia)',
'so-KE': 'Somali (Kenya)',
'so-ET': 'Somali (Ethiopia)',
'so-DJ': 'Somali (Djibouti)',
'sq': 'Albanian',
'sq-XK': 'Albanian (Kosovo)',
'sq-MK': 'Albanian (North Macedonia)',
'sq-AL': 'Albanian (Albania)',
'sr': 'Serbian',
'sr-Latn-XK': 'Serbian (Latin, Kosovo)',
'sr-Latn-RS': 'Serbian (Latin, Serbia)',
'sr-Latn-ME': 'Montenegrin (Latin)',
'sr-Latn-BA': 'Serbian (Latin, Bosnia & Herzegovina)',
'sr-Latn': 'Serbian (Latin)',
'sr-Cyrl-XK': 'Serbian (Cyrillic, Kosovo)',
'sr-Cyrl-RS': 'Serbian (Cyrillic, Serbia)',
'sr-Cyrl-ME': 'Montenegrin (Cyrillic)',
'sr-Cyrl-BA': 'Serbian (Cyrillic, Bosnia & Herzegovina)',
'sr-Cyrl': 'Serbian (Cyrillic)',
'su': 'Sundanese',
'su-Latn-ID': 'Sundanese (Latin, Indonesia)',
'su-Latn': 'Sundanese (Latin)',
'sv': 'Swedish',
'sv-SE': 'Swedish (Sweden)',
'sv-FI': 'Swedish (Finland)',
'sv-AX': 'Swedish (Åland Islands)',
'sw': 'Swahili',
'sw-UG': 'Swahili (Uganda)',
'sw-TZ': 'Swahili (Tanzania)',
'sw-KE': 'Swahili (Kenya)',
'sw-CD': 'Congo Swahili',
'ta': 'Tamil',
'ta-SG': 'Tamil (Singapore)',
'ta-MY': 'Tamil (Malaysia)',
'ta-LK': 'Tamil (Sri Lanka)',
'ta-IN': 'Tamil (India)',
'te': 'Telugu',
'te-IN': 'Telugu (India)',
'teo': 'Teso',
'teo-UG': 'Teso (Uganda)',
'teo-KE': 'Teso (Kenya)',
'tg': 'Tajik',
'tg-TJ': 'Tajik (Tajikistan)',
'th': 'Thai',
'th-TH': 'Thai (Thailand)',
'ti': 'Tigrinya',
'ti-ET': 'Tigrinya (Ethiopia)',
'ti-ER': 'Tigrinya (Eritrea)',
'tk': 'Turkmen',
'tk-TM': 'Turkmen (Turkmenistan)',
'to': 'Tongan',
'to-TO': 'Tongan (Tonga)',
'tr': 'Turkish',
'tr-TR': 'Turkish (Turkey)',
'tr-CY': 'Turkish (Cyprus)',
'tt': 'Tatar',
'tt-RU': 'Tatar (Russia)',
'twq': 'Tasawaq',
'twq-NE': 'Tasawaq (Niger)',
'tzm': 'Central Atlas Tamazight',
'tzm-MA': 'Central Atlas Tamazight (Morocco)',
'ug': 'Uyghur',
'ug-CN': 'Uyghur (China)',
'uk': 'Ukrainian',
'uk-UA': 'Ukrainian (Ukraine)',
'ur': 'Urdu',
'ur-PK': 'Urdu (Pakistan)',
'ur-IN': 'Urdu (India)',
'uz': 'Uzbek',
'uz-Latn-UZ': 'Uzbek (Latin, Uzbekistan)',
'uz-Latn': 'Uzbek (Latin)',
'uz-Cyrl-UZ': 'Uzbek (Cyrillic, Uzbekistan)',
'uz-Cyrl': 'Uzbek (Cyrillic)',
'uz-Arab-AF': 'Uzbek (Arabic, Afghanistan)',
'uz-Arab': 'Uzbek (Arabic)',
'vai': 'Vai',
'vai-Vaii-LR': 'Vai (Vai, Liberia)',
'vai-Vaii': 'Vai (Vai)',
'vai-Latn-LR': 'Vai (Latin, Liberia)',
'vai-Latn': 'Vai (Latin)',
'vi': 'Vietnamese',
'vi-VN': 'Vietnamese (Vietnam)',
'vun': 'Vunjo',
'vun-TZ': 'Vunjo (Tanzania)',
'wae': 'Walser',
'wae-CH': 'Walser (Switzerland)',
'wo': 'Wolof',
'wo-SN': 'Wolof (Senegal)',
'xh': 'Xhosa',
'xh-ZA': 'Xhosa (South Africa)',
'xog': 'Soga',
'xog-UG': 'Soga (Uganda)',
'yav': 'Yangben',
'yav-CM': 'Yangben (Cameroon)',
'yi': 'Yiddish',
'yi-001': 'Yiddish (world)',
'yo': 'Yoruba',
'yo-NG': 'Yoruba (Nigeria)',
'yo-BJ': 'Yoruba (Benin)',
'yrl': 'Nheengatu',
'yrl-VE': 'Nheengatu (Venezuela)',
'yrl-CO': 'Nheengatu (Colombia)',
'yrl-BR': 'Nheengatu (Brazil)',
'zgh': 'Standard Moroccan Tamazight',
'zgh-MA': 'Standard Moroccan Tamazight (Morocco)',
'zh': 'Chinese',
'zh-Hans-CN': 'Simplified Chinese (China)',
'zh-Hans-HK': 'Simplified Chinese (Hong Kong SAR China)',
'zh-Hans-MO': 'Simplified Chinese (Macao SAR China)',
'zh-Hans-SG': 'Simplified Chinese (Singapore)',
'zh-Hans': 'Simplified Chinese',
'zh-Hant-TW': 'Traditional Chinese (Taiwan SAR China)',
'zh-Hant-MO': 'Traditional Chinese (Macao SAR China)',
'zh-Hant-HK': 'Traditional Chinese (Hong Kong SAR China)',
'zh-Hant': 'Traditional Chinese',
'zu': 'Zulu',
'zu-ZA': 'Zulu (South Africa)',
},
error: {
'editor_dirty': 'Unsaved content is currently detected, please save the project first',
'translate_api_error': 'Error in translation request, please report',
'unknown_file_type': 'Unknown file type: {}',
'target_translate_data_not_found': 'Target language not found: {}',
'target_translate_data_exist': 'Target language already exists: {}',
'local_language_not_set': 'No local development language set',
'translate_provider_config_not_found': 'Translator credentials not set',
'provider_tag_not_found': 'Please first select a language for service provider identification',
'translate_item_not_found': 'No translation data found: {}',
'scan_option_empty': 'Scan options are empty',
'provider_input_error': '{}',
'auto_translate_network_error': 'Network error: {}',
'po_file_language_not_equal': 'Imported PO file\'s [Language] not equal to target language',
'unavailable_csv_file': 'Unavailable CSV file {}',
'unavailable_xlsx_file': 'Unavailable XLSX file {}',
'unknown_error': 'Unknown error',
'merge_different_key': 'Attempt to merge data with two different keys',
'index_translate_data_not_found': 'Corrupted data, not found localization-editor/index.yaml',
'scene_error': 'Unknown scene error, please try to re-import the plugin resources or restart the editor',
'invalid_translate_data': 'Invalid translate data: {}',
'invalid_translate_file_content': 'Invalid translate file content: {}',
"YOUDAO": {
'error': 'YouDao error: {errorCode}, {message}',
'errorCode': 'YouDao error code: {errorCode}, please see the corresponding information in [error code list] in https://ai.youdao.com/DOCSIRMA/html/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E7%BF%BB%E8%AF%91/API%E6%96%87%E6%A1%A3/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1-API%E6%96%87%E6%A1%A3.html',
'207': 'Replay request',
'301': 'Thesaurus query failed',
'302': 'Translation query failed',
'1412': 'Maximum number of recognized bytes exceeded',
'2004': 'Synthesis character is too long',
'2412': 'Exceeded the maximum number of requested characters',
'3412': 'Exceeded the maximum number of requested characters',
}
},
};

View File

@@ -0,0 +1,980 @@
'use strict';
module.exports = {
open_panel: '翻译',
'localization-editor': '本地化编辑器',
description: '本地化编辑器',
service_provider: '译文服务商',
collection: '收集并统计',
language_compilation: '语言编译',
select_placeholder: '请选择',
unselect_service_tip: '未选择译文服务商,将不支持自动翻译功能',
local_language: '本地开发语言:',
collected_from_resource_files: '从资源文件中收集',
required: '*必选',
file_no_exist: '文件不存在',
cannot_empty: '不能为空',
common_languages: '常用的语言',
cancel: '取消',
confirm: '确定',
localization_editor_project_config: '本地化项目配置',
localization_editor_editor_config: '本地化编辑器配置',
localization_editor_provider_config: '本地化翻译服务商配置',
ask_delete: '是否删除?',
loading_tips: '加载中...',
component: {
help: `https://docs.cocos.com/creator/manual/zh/editor/l10n/l10n-label.html`,
string: 'Label 组件内文本',
count: '用于获取变体的数字',
key: '本地化的键值',
},
YOUDAO: {
'zh-CHS': '简体中文',
'zh-CHT': '繁体中文',
},
label_inspector: {
no_origin: '无原文',
key: '唯一标识符',
'key:': '唯一标识符:',
save: '保存',
reset: '重置',
saving: '保存中...',
cannot_empty: '不能为空',
error_tooltip: '包含字母、数字、下划线、连字符、@、/、+、| 、.\n不能为其他字符',
exist_media_tooltip: '该 key 为资源的 key',
edit: '编辑',
},
build: {
compress_warning: '因 uuid 为 "${a}" 的图片与 uuid 为 "${b}" 的图片的压缩纹理选项不一致,导致构建失败,请设置为一致',
packable_warning: 'uuid 为 \'${uuid}\' 的图片由于开启了合图功能,该图片的翻译失效',
use_polyfill: '使用的 polyfill 脚本',
use_language: '使用的语言',
select_all: '全选',
default_language: '默认语言',
fallback_language: '备用语言',
},
home: {
po_name: 'PO 文件',
csv_name: 'CSV',
xlsx_name: 'Excel 工作簿 ',
service_provider: '译文服务商',
collection: '收集',
language_compilation: '语言编译',
select_placeholder: '请选择',
unselect_service_tip: '未选择译文服务商,将不支持自动翻译功能',
'local_language:': '本地开发语言:',
collected_from_resource_files: '从资源文件中收集',
required: '*必选',
delete: '删除',
preview: '预览',
combine: '编译',
translate: '翻译',
complement: '补全',
not_recorded: '未统计',
collect_and_count: '收集并统计',
add_new_language: '添加新语言',
add: '添加',
select: '请选择',
language: '语言',
'language:': '语言:',
extname: '扩展名',
exclude_path: '排除路径',
language_for_service_provider: '服务商识别的语言',
dir: '目录',
search_dir: '搜索目录',
translate_process: ' 翻译进度',
combine_process: '编译进度',
operation: '操作',
local_language: '本地开发语言',
combine_tooltip: '文本按实际字数计算, 资源按单位1计算',
translate_tooltip: '任意条目以单位1计算',
count: '字数:',
collecting: '正在统计中',
unselect: '未选择开发语言',
save: '保存',
complete: '完成',
collect_group: '收集组',
turn_on_tip: '请先启用 Localization Editor 工具 ',
turn_on: '启用 Localization Editor',
turn_off_warning: '关闭后所有的翻译数据及配置将被清空,确定要关闭?',
turn_off: '关闭 L10N',
delete_data: '清空数据',
delete_data_warning: '清空后翻译的数据将会永久丢失,确定要清空数据?',
export: '导出',
export_all: '导出全部',
},
translate: {
source_string_placeholder: '请输入资源的 URL 上将被替换的内容',
dist_string_placeholder: '请输入资源的 URL 上替换后的内容',
source_string: '查找内容:',
dist_string: '目标内容:',
new_value: '新值',
old_value: '原值',
import_po: '导入 PO 文件',
export_po: '导出 PO 文件',
save: '保存',
unfilled: '未填充',
untranslated: '未翻译',
filled: '已填充',
count: '字数:',
translate: '翻译',
translated: '已翻译',
key: '唯一标识符',
'key:': '唯一标识符:',
origin: '原文',
target: '译文',
import_all: '智能匹配',
import_file: '导入文件',
import: '导入',
jump: '跳过',
'position:': '位置:',
reference_uuid: '引用的节点的 uuid:',
variant: '变体',
after_variant: '变体后',
standard: '标准',
delete_variant: '删除变体',
cancel: '取消',
confirm: '确定',
cover: '覆盖',
conflict_dialog_title: '注意:',
conflict_dialog_content: '【已翻译】里已存在以下数据,覆盖后原数据将会消失。确定要覆盖吗',
unsaved_warning: '当前翻译的内容是否需要保存?',
delete_warning: '确定删除吗?',
auto_import_warning: '即将对{length}个文件进行智能匹配, 匹配后会使用 {targetLocale} (目标语言)替换 {localLocale} (源语言) ,确定要执行?',
quit_warning: '尚有未保存的内容, 确定退出吗?',
import_tab_title: '导入的文件',
import_file_conflicts_with_file_warning: '{num}个数据因为key相同导致其无法导入请问如何处理',
saving_tips: '进行中...',
},
language_code: {
'af': '南非荷兰语',
'af-ZA': '南非荷兰语(南非)',
'af-NA': '南非荷兰语(纳米比亚)',
'agq': '亚罕语',
'agq-CM': '亚罕语(喀麦隆)',
'ak': '阿肯语',
'ak-GH': '阿肯语(加纳)',
'am': '阿姆哈拉语',
'am-ET': '阿姆哈拉语(埃塞俄比亚)',
'ar': '阿拉伯语',
'ar-YE': '阿拉伯语(也门)',
'ar-TN': '阿拉伯语(突尼斯)',
'ar-TD': '阿拉伯语(乍得)',
'ar-SY': '阿拉伯语(叙利亚)',
'ar-SS': '阿拉伯语(南苏丹)',
'ar-SO': '阿拉伯语(索马里)',
'ar-SD': '阿拉伯语(苏丹)',
'ar-SA': '阿拉伯语(沙特阿拉伯)',
'ar-QA': '阿拉伯语(卡塔尔)',
'ar-PS': '阿拉伯语(巴勒斯坦领土)',
'ar-OM': '阿拉伯语(阿曼)',
'ar-MR': '阿拉伯语(毛里塔尼亚)',
'ar-MA': '阿拉伯语(摩洛哥)',
'ar-LY': '阿拉伯语(利比亚)',
'ar-LB': '阿拉伯语(黎巴嫩)',
'ar-KW': '阿拉伯语(科威特)',
'ar-KM': '阿拉伯语(科摩罗)',
'ar-JO': '阿拉伯语(约旦)',
'ar-IQ': '阿拉伯语(伊拉克)',
'ar-IL': '阿拉伯语(以色列)',
'ar-ER': '阿拉伯语(厄立特里亚)',
'ar-EH': '阿拉伯语(西撒哈拉)',
'ar-EG': '阿拉伯语(埃及)',
'ar-DZ': '阿拉伯语(阿尔及利亚)',
'ar-DJ': '阿拉伯语(吉布提)',
'ar-BH': '阿拉伯语(巴林)',
'ar-AE': '阿拉伯语(阿拉伯联合酋长国)',
'ar-001': '现代标准阿拉伯语',
'as': '阿萨姆语',
'as-IN': '阿萨姆语(印度)',
'asa': '帕雷语',
'asa-TZ': '帕雷语(坦桑尼亚)',
'ast': '阿斯图里亚斯语',
'ast-ES': '阿斯图里亚斯语(西班牙)',
'az': '阿塞拜疆语',
'az-Latn-AZ': '阿塞拜疆语(拉丁文,阿塞拜疆)',
'az-Latn': '阿塞拜疆语(拉丁文)',
'az-Cyrl-AZ': '阿塞拜疆语(西里尔文,阿塞拜疆)',
'az-Cyrl': '阿塞拜疆语(西里尔文)',
'bas': '巴萨语',
'bas-CM': '巴萨语(喀麦隆)',
'be': '白俄罗斯语',
'be-TARASK': '白俄罗斯语Taraskievica 拼字)',
'be-BY': '白俄罗斯语(白俄罗斯)',
'bem': '本巴语',
'bem-ZM': '本巴语(赞比亚)',
'bez': '贝纳语',
'bez-TZ': '贝纳语(坦桑尼亚)',
'bg': '保加利亚语',
'bg-BG': '保加利亚语(保加利亚)',
'bm': '班巴拉语',
'bm-ML': '班巴拉语(马里)',
'bn': '孟加拉语',
'bn-IN': '孟加拉语(印度)',
'bn-BD': '孟加拉语(孟加拉国)',
'br': '布列塔尼语',
'br-FR': '布列塔尼语(法国)',
'brx': '博多语',
'brx-IN': '博多语(印度)',
'bs': '波斯尼亚语',
'bs-Latn-BA': '波斯尼亚语(拉丁文,波斯尼亚和黑塞哥维那)',
'bs-Latn': '波斯尼亚语(拉丁文)',
'bs-Cyrl-BA': '波斯尼亚语(西里尔文,波斯尼亚和黑塞哥维那)',
'bs-Cyrl': '波斯尼亚语(西里尔文)',
'ca': '加泰罗尼亚语',
'ca-IT': '加泰罗尼亚语(意大利)',
'ca-FR': '加泰罗尼亚语(法国)',
'ca-ES-VALENCIA': '加泰罗尼亚语(西班牙,瓦伦西亚文)',
'ca-ES': '加泰罗尼亚语(西班牙)',
'ca-AD': '加泰罗尼亚语(安道尔)',
'ccp': '查克玛语',
'ccp-IN': '查克玛语(印度)',
'ccp-BD': '查克玛语(孟加拉国)',
'ce': '车臣语',
'ce-RU': '车臣语(俄罗斯)',
'ceb': '宿务语',
'ceb-PH': '宿务语(菲律宾)',
'cgg': '奇加语',
'cgg-UG': '奇加语(乌干达)',
'chr': '切罗基语',
'chr-US': '切罗基语(美国)',
'ckb': '中库尔德语',
'ckb-IR': '中库尔德语(伊朗)',
'ckb-IQ': '中库尔德语(伊拉克)',
'cs': '捷克语',
'cs-CZ': '捷克语(捷克)',
'cy': '威尔士语',
'cy-GB': '威尔士语(英国)',
'da': '丹麦语',
'da-GL': '丹麦语(格陵兰)',
'da-DK': '丹麦语(丹麦)',
'dav': '台塔语',
'dav-KE': '台塔语(肯尼亚)',
'de': '德语',
'de-LU': '德语(卢森堡)',
'de-LI': '德语(列支敦士登)',
'de-IT': '德语(意大利)',
'de-DE': '德语(德国)',
'de-CH': '瑞士高地德语',
'de-BE': '德语(比利时)',
'de-AT': '奥地利德语',
'dje': '哲尔马语',
'dje-NE': '哲尔马语(尼日尔)',
'doi': '多格拉语',
'doi-IN': '多格拉语(印度)',
'dsb': '下索布语',
'dsb-DE': '下索布语(德国)',
'dua': '杜阿拉语',
'dua-CM': '杜阿拉语(喀麦隆)',
'dyo': '朱拉语',
'dyo-SN': '朱拉语(塞内加尔)',
'dz': '宗卡语',
'dz-BT': '宗卡语(不丹)',
'ebu': '恩布语',
'ebu-KE': '恩布语(肯尼亚)',
'ee': '埃维语',
'ee-TG': '埃维语(多哥)',
'ee-GH': '埃维语(加纳)',
'el': '希腊语',
'el-GR': '希腊语(希腊)',
'el-CY': '希腊语(塞浦路斯)',
'en': '英语',
'en-ZW': '英语(津巴布韦)',
'en-ZM': '英语(赞比亚)',
'en-ZA': '英语(南非)',
'en-WS': '英语(萨摩亚)',
'en-VU': '英语(瓦努阿图)',
'en-VI': '英语(美属维尔京群岛)',
'en-VG': '英语(英属维尔京群岛)',
'en-VC': '英语(圣文森特和格林纳丁斯)',
'en-US-POSIX': '美国英语(电脑)',
'en-US': '美国英语',
'en-UM': '英语(美国本土外小岛屿)',
'en-UG': '英语(乌干达)',
'en-TZ': '英语(坦桑尼亚)',
'en-TV': '英语(图瓦卢)',
'en-TT': '英语(特立尼达和多巴哥)',
'en-TO': '英语(汤加)',
'en-TK': '英语(托克劳)',
'en-TC': '英语(特克斯和凯科斯群岛)',
'en-SZ': '英语(斯威士兰)',
'en-SX': '英语(荷属圣马丁)',
'en-SS': '英语(南苏丹)',
'en-SL': '英语(塞拉利昂)',
'en-SI': '英语(斯洛文尼亚)',
'en-SH': '英语(圣赫勒拿)',
'en-SG': '英语(新加坡)',
'en-SE': '英语(瑞典)',
'en-SD': '英语(苏丹)',
'en-SC': '英语(塞舌尔)',
'en-SB': '英语(所罗门群岛)',
'en-RW': '英语(卢旺达)',
'en-PW': '英语(帕劳)',
'en-PR': '英语(波多黎各)',
'en-PN': '英语(皮特凯恩群岛)',
'en-PK': '英语(巴基斯坦)',
'en-PH': '英语(菲律宾)',
'en-PG': '英语(巴布亚新几内亚)',
'en-NZ': '英语(新西兰)',
'en-NU': '英语(纽埃)',
'en-NR': '英语(瑙鲁)',
'en-NL': '英语(荷兰)',
'en-NG': '英语(尼日利亚)',
'en-NF': '英语(诺福克岛)',
'en-NA': '英语(纳米比亚)',
'en-MY': '英语(马来西亚)',
'en-MW': '英语(马拉维)',
'en-MV': '英语(马尔代夫)',
'en-MU': '英语(毛里求斯)',
'en-MT': '英语(马耳他)',
'en-MS': '英语(蒙特塞拉特)',
'en-MP': '英语(北马里亚纳群岛)',
'en-MO': '英语(中国澳门特别行政区)',
'en-MH': '英语(马绍尔群岛)',
'en-MG': '英语(马达加斯加)',
'en-LS': '英语(莱索托)',
'en-LR': '英语(利比里亚)',
'en-LC': '英语(圣卢西亚)',
'en-KY': '英语(开曼群岛)',
'en-KN': '英语(圣基茨和尼维斯)',
'en-KI': '英语(基里巴斯)',
'en-KE': '英语(肯尼亚)',
'en-JM': '英语(牙买加)',
'en-JE': '英语(泽西岛)',
'en-IO': '英语(英属印度洋领地)',
'en-IN': '英语(印度)',
'en-IM': '英语(马恩岛)',
'en-IL': '英语(以色列)',
'en-IE': '英语(爱尔兰)',
'en-HK': '英语(中国香港特别行政区)',
'en-GY': '英语(圭亚那)',
'en-GU': '英语(关岛)',
'en-GM': '英语(冈比亚)',
'en-GI': '英语(直布罗陀)',
'en-GH': '英语(加纳)',
'en-GG': '英语(根西岛)',
'en-GD': '英语(格林纳达)',
'en-GB': '英国英语',
'en-FM': '英语(密克罗尼西亚)',
'en-FK': '英语(福克兰群岛)',
'en-FJ': '英语(斐济)',
'en-FI': '英语(芬兰)',
'en-ER': '英语(厄立特里亚)',
'en-DM': '英语(多米尼克)',
'en-DK': '英语(丹麦)',
'en-DG': '英语(迪戈加西亚岛)',
'en-DE': '英语(德国)',
'en-CY': '英语(塞浦路斯)',
'en-CX': '英语(圣诞岛)',
'en-CM': '英语(喀麦隆)',
'en-CK': '英语(库克群岛)',
'en-CH': '英语(瑞士)',
'en-CC': '英语(科科斯[基林]群岛)',
'en-CA': '加拿大英语',
'en-BZ': '英语(伯利兹)',
'en-BW': '英语(博茨瓦纳)',
'en-BS': '英语(巴哈马)',
'en-BM': '英语(百慕大)',
'en-BI': '英语(布隆迪)',
'en-BE': '英语(比利时)',
'en-BB': '英语(巴巴多斯)',
'en-AU': '澳大利亚英语',
'en-AT': '英语(奥地利)',
'en-AS': '英语(美属萨摩亚)',
'en-AI': '英语(安圭拉)',
'en-AG': '英语(安提瓜和巴布达)',
'en-AE': '英语(阿拉伯联合酋长国)',
'en-150': '英语(欧洲)',
'en-001': '英语(世界)',
'eo': '世界语',
'eo-001': '世界语(世界)',
'es': '西班牙语',
'es-VE': '西班牙语(委内瑞拉)',
'es-UY': '西班牙语(乌拉圭)',
'es-US': '西班牙语(美国)',
'es-SV': '西班牙语(萨尔瓦多)',
'es-PY': '西班牙语(巴拉圭)',
'es-PR': '西班牙语(波多黎各)',
'es-PH': '西班牙语(菲律宾)',
'es-PE': '西班牙语(秘鲁)',
'es-PA': '西班牙语(巴拿马)',
'es-NI': '西班牙语(尼加拉瓜)',
'es-MX': '墨西哥西班牙语',
'es-IC': '西班牙语(加纳利群岛)',
'es-HN': '西班牙语(洪都拉斯)',
'es-GT': '西班牙语(危地马拉)',
'es-GQ': '西班牙语(赤道几内亚)',
'es-ES': '欧洲西班牙语',
'es-EC': '西班牙语(厄瓜多尔)',
'es-EA': '西班牙语(休达及梅利利亚)',
'es-DO': '西班牙语(多米尼加共和国)',
'es-CU': '西班牙语(古巴)',
'es-CR': '西班牙语(哥斯达黎加)',
'es-CO': '西班牙语(哥伦比亚)',
'es-CL': '西班牙语(智利)',
'es-BZ': '西班牙语(伯利兹)',
'es-BR': '西班牙语(巴西)',
'es-BO': '西班牙语(玻利维亚)',
'es-AR': '西班牙语(阿根廷)',
'es-419': '拉丁美洲西班牙语',
'et': '爱沙尼亚语',
'et-EE': '爱沙尼亚语(爱沙尼亚)',
'eu': '巴斯克语',
'eu-ES': '巴斯克语(西班牙)',
'ewo': '埃翁多语',
'ewo-CM': '埃翁多语(喀麦隆)',
'fa': '波斯语',
'fa-IR': '波斯语(伊朗)',
'fa-AF': '达里语',
'ff': '富拉语',
'ff-Latn-SN': '富拉语(拉丁文,塞内加尔)',
'ff-Latn-SL': '富拉语(拉丁文,塞拉利昂)',
'ff-Latn-NG': '富拉语(拉丁文,尼日利亚)',
'ff-Latn-NE': '富拉语(拉丁文,尼日尔)',
'ff-Latn-MR': '富拉语(拉丁文,毛里塔尼亚)',
'ff-Latn-LR': '富拉语(拉丁文,利比里亚)',
'ff-Latn-GW': '富拉语(拉丁文,几内亚比绍)',
'ff-Latn-GN': '富拉语(拉丁文,几内亚)',
'ff-Latn-GM': '富拉语(拉丁文,冈比亚)',
'ff-Latn-GH': '富拉语(拉丁文,加纳)',
'ff-Latn-CM': '富拉语(拉丁文,喀麦隆)',
'ff-Latn-BF': '富拉语(拉丁文,布基纳法索)',
'ff-Latn': '富拉语(拉丁文)',
'ff-Adlm-SN': '富拉语(阿德拉姆文,塞内加尔)',
'ff-Adlm-SL': '富拉语(阿德拉姆文,塞拉利昂)',
'ff-Adlm-NG': '富拉语(阿德拉姆文,尼日利亚)',
'ff-Adlm-NE': '富拉语(阿德拉姆文,尼日尔)',
'ff-Adlm-MR': '富拉语(阿德拉姆文,毛里塔尼亚)',
'ff-Adlm-LR': '富拉语(阿德拉姆文,利比里亚)',
'ff-Adlm-GW': '富拉语(阿德拉姆文,几内亚比绍)',
'ff-Adlm-GN': '富拉语(阿德拉姆文,几内亚)',
'ff-Adlm-GM': '富拉语(阿德拉姆文,冈比亚)',
'ff-Adlm-GH': '富拉语(阿德拉姆文,加纳)',
'ff-Adlm-CM': '富拉语(阿德拉姆文,喀麦隆)',
'ff-Adlm-BF': '富拉语(阿德拉姆文,布基纳法索)',
'ff-Adlm': '富拉语(阿德拉姆文)',
'fi': '芬兰语',
'fi-FI': '芬兰语(芬兰)',
'fil': '菲律宾语',
'fil-PH': '菲律宾语(菲律宾)',
'fo': '法罗语',
'fo-FO': '法罗语(法罗群岛)',
'fo-DK': '法罗语(丹麦)',
'fr': '法语',
'fr-YT': '法语(马约特)',
'fr-WF': '法语(瓦利斯和富图纳)',
'fr-VU': '法语(瓦努阿图)',
'fr-TN': '法语(突尼斯)',
'fr-TG': '法语(多哥)',
'fr-TD': '法语(乍得)',
'fr-SY': '法语(叙利亚)',
'fr-SN': '法语(塞内加尔)',
'fr-SC': '法语(塞舌尔)',
'fr-RW': '法语(卢旺达)',
'fr-RE': '法语(留尼汪)',
'fr-PM': '法语(圣皮埃尔和密克隆群岛)',
'fr-PF': '法语(法属波利尼西亚)',
'fr-NE': '法语(尼日尔)',
'fr-NC': '法语(新喀里多尼亚)',
'fr-MU': '法语(毛里求斯)',
'fr-MR': '法语(毛里塔尼亚)',
'fr-MQ': '法语(马提尼克)',
'fr-ML': '法语(马里)',
'fr-MG': '法语(马达加斯加)',
'fr-MF': '法语(法属圣马丁)',
'fr-MC': '法语(摩纳哥)',
'fr-MA': '法语(摩洛哥)',
'fr-LU': '法语(卢森堡)',
'fr-KM': '法语(科摩罗)',
'fr-HT': '法语(海地)',
'fr-GQ': '法语(赤道几内亚)',
'fr-GP': '法语(瓜德罗普)',
'fr-GN': '法语(几内亚)',
'fr-GF': '法语(法属圭亚那)',
'fr-GA': '法语(加蓬)',
'fr-FR': '法语(法国)',
'fr-DZ': '法语(阿尔及利亚)',
'fr-DJ': '法语(吉布提)',
'fr-CM': '法语(喀麦隆)',
'fr-CI': '法语(科特迪瓦)',
'fr-CH': '瑞士法语',
'fr-CG': '法语(刚果[布])',
'fr-CF': '法语(中非共和国)',
'fr-CD': '法语(刚果[金])',
'fr-CA': '加拿大法语',
'fr-BL': '法语(圣巴泰勒米)',
'fr-BJ': '法语(贝宁)',
'fr-BI': '法语(布隆迪)',
'fr-BF': '法语(布基纳法索)',
'fr-BE': '法语(比利时)',
'fur': '弗留利语',
'fur-IT': '弗留利语(意大利)',
'fy': '西弗里西亚语',
'fy-NL': '西弗里西亚语(荷兰)',
'ga': '爱尔兰语',
'ga-IE': '爱尔兰语(爱尔兰)',
'ga-GB': '爱尔兰语(英国)',
'gd': '苏格兰盖尔语',
'gd-GB': '苏格兰盖尔语(英国)',
'gl': '加利西亚语',
'gl-ES': '加利西亚语(西班牙)',
'gsw': '瑞士德语',
'gsw-LI': '瑞士德语(列支敦士登)',
'gsw-FR': '瑞士德语(法国)',
'gsw-CH': '瑞士德语(瑞士)',
'gu': '古吉拉特语',
'gu-IN': '古吉拉特语(印度)',
'guz': '古西语',
'guz-KE': '古西语(肯尼亚)',
'gv': '马恩语',
'gv-IM': '马恩语(马恩岛)',
'ha': '豪萨语',
'ha-NG': '豪萨语(尼日利亚)',
'ha-NE': '豪萨语(尼日尔)',
'ha-GH': '豪萨语(加纳)',
'haw': '夏威夷语',
'haw-US': '夏威夷语(美国)',
'he': '希伯来语',
'he-IL': '希伯来语(以色列)',
'hi': '印地语',
'hi-Latn-IN': '印地语(拉丁文,印度)',
'hi-Latn': '印地语(拉丁文)',
'hi-IN': '印地语(印度)',
'hr': '克罗地亚语',
'hr-HR': '克罗地亚语(克罗地亚)',
'hr-BA': '克罗地亚语(波斯尼亚和黑塞哥维那)',
'hsb': '上索布语',
'hsb-DE': '上索布语(德国)',
'hu': '匈牙利语',
'hu-HU': '匈牙利语(匈牙利)',
'hy': '亚美尼亚语',
'hy-AM': '亚美尼亚语(亚美尼亚)',
'ia': '国际语',
'ia-001': '国际语(世界)',
'id': '印度尼西亚语',
'id-ID': '印度尼西亚语(印度尼西亚)',
'ig': '伊博语',
'ig-NG': '伊博语(尼日利亚)',
'is': '冰岛语',
'is-IS': '冰岛语(冰岛)',
'it': '意大利语',
'it-VA': '意大利语(梵蒂冈)',
'it-SM': '意大利语(圣马力诺)',
'it-IT': '意大利语(意大利)',
'it-CH': '意大利语(瑞士)',
'ja': '日语',
'ja-JP': '日语(日本)',
'jgo': '恩艮巴语',
'jgo-CM': '恩艮巴语(喀麦隆)',
'jmc': '马切姆语',
'jmc-TZ': '马切姆语(坦桑尼亚)',
'jv': '爪哇语',
'jv-ID': '爪哇语(印度尼西亚)',
'ka': '格鲁吉亚语',
'ka-GE': '格鲁吉亚语(格鲁吉亚)',
'kab': '卡拜尔语',
'kab-DZ': '卡拜尔语(阿尔及利亚)',
'kam': '卡姆巴语',
'kam-KE': '卡姆巴语(肯尼亚)',
'kde': '马孔德语',
'kde-TZ': '马孔德语(坦桑尼亚)',
'kea': '卡布佛得鲁语',
'kea-CV': '卡布佛得鲁语(佛得角)',
'kgp': 'kgp',
'kgp-BR': 'kgp巴西',
'khq': '西桑海语',
'khq-ML': '西桑海语(马里)',
'ki': '吉库尤语',
'ki-KE': '吉库尤语(肯尼亚)',
'kk': '哈萨克语',
'kk-KZ': '哈萨克语(哈萨克斯坦)',
'kkj': '卡库语',
'kkj-CM': '卡库语(喀麦隆)',
'kl': '格陵兰语',
'kl-GL': '格陵兰语(格陵兰)',
'kln': '卡伦金语',
'kln-KE': '卡伦金语(肯尼亚)',
'km': '高棉语',
'km-KH': '高棉语(柬埔寨)',
'kn': '卡纳达语',
'kn-IN': '卡纳达语(印度)',
'ko': '韩语',
'ko-KR': '韩语(韩国)',
'ko-KP': '韩语(朝鲜)',
'kok': '孔卡尼语',
'kok-IN': '孔卡尼语(印度)',
'ks': '克什米尔语',
'ks-Deva-IN': '克什米尔语(天城文,印度)',
'ks-Deva': '克什米尔语(天城文)',
'ks-Arab-IN': '克什米尔语(阿拉伯文,印度)',
'ks-Arab': '克什米尔语(阿拉伯文)',
'ksb': '香巴拉语',
'ksb-TZ': '香巴拉语(坦桑尼亚)',
'ksf': '巴菲亚语',
'ksf-CM': '巴菲亚语(喀麦隆)',
'ksh': '科隆语',
'ksh-DE': '科隆语(德国)',
'ku': '库尔德语',
'ku-TR': '库尔德语(土耳其)',
'kw': '康沃尔语',
'kw-GB': '康沃尔语(英国)',
'ky': '柯尔克孜语',
'ky-KG': '柯尔克孜语(吉尔吉斯斯坦)',
'lag': '朗吉语',
'lag-TZ': '朗吉语(坦桑尼亚)',
'lb': '卢森堡语',
'lb-LU': '卢森堡语(卢森堡)',
'lg': '卢干达语',
'lg-UG': '卢干达语(乌干达)',
'lkt': '拉科塔语',
'lkt-US': '拉科塔语(美国)',
'ln': '林加拉语',
'ln-CG': '林加拉语(刚果[布])',
'ln-CF': '林加拉语(中非共和国)',
'ln-CD': '林加拉语(刚果[金])',
'ln-AO': '林加拉语(安哥拉)',
'lo': '老挝语',
'lo-LA': '老挝语(老挝)',
'lrc': '北卢尔语',
'lrc-IR': '北卢尔语(伊朗)',
'lrc-IQ': '北卢尔语(伊拉克)',
'lt': '立陶宛语',
'lt-LT': '立陶宛语(立陶宛)',
'lu': '鲁巴加丹加语',
'lu-CD': '鲁巴加丹加语(刚果[金])',
'luo': '卢奥语',
'luo-KE': '卢奥语(肯尼亚)',
'luy': '卢雅语',
'luy-KE': '卢雅语(肯尼亚)',
'lv': '拉脱维亚语',
'lv-LV': '拉脱维亚语(拉脱维亚)',
'mai': '迈蒂利语',
'mai-IN': '迈蒂利语(印度)',
'mas': '马赛语',
'mas-TZ': '马赛语(坦桑尼亚)',
'mas-KE': '马赛语(肯尼亚)',
'mer': '梅鲁语',
'mer-KE': '梅鲁语(肯尼亚)',
'mfe': '毛里求斯克里奥尔语',
'mfe-MU': '毛里求斯克里奥尔语(毛里求斯)',
'mg': '马拉加斯语',
'mg-MG': '马拉加斯语(马达加斯加)',
'mgh': '马库阿语',
'mgh-MZ': '马库阿语(莫桑比克)',
'mgo': '梅塔语',
'mgo-CM': '梅塔语(喀麦隆)',
'mi': '毛利语',
'mi-NZ': '毛利语(新西兰)',
'mk': '马其顿语',
'mk-MK': '马其顿语(北马其顿)',
'ml': '马拉雅拉姆语',
'ml-IN': '马拉雅拉姆语(印度)',
'mn': '蒙古语',
'mn-MN': '蒙古语(蒙古)',
'mni': '曼尼普尔语',
'mni-Beng-IN': '曼尼普尔语(孟加拉文,印度)',
'mni-Beng': '曼尼普尔语(孟加拉文)',
'mr': '马拉地语',
'mr-IN': '马拉地语(印度)',
'ms': '马来语',
'ms-SG': '马来语(新加坡)',
'ms-MY': '马来语(马来西亚)',
'ms-ID': '马来语(印度尼西亚)',
'ms-BN': '马来语(文莱)',
'mt': '马耳他语',
'mt-MT': '马耳他语(马耳他)',
'mua': '蒙当语',
'mua-CM': '蒙当语(喀麦隆)',
'my': '缅甸语',
'my-MM': '缅甸语(缅甸)',
'mzn': '马赞德兰语',
'mzn-IR': '马赞德兰语(伊朗)',
'naq': '纳马语',
'naq-NA': '纳马语(纳米比亚)',
'nb': '书面挪威语',
'nb-SJ': '书面挪威语(斯瓦尔巴和扬马延)',
'nb-NO': '书面挪威语(挪威)',
'nd': '北恩德贝勒语',
'nd-ZW': '北恩德贝勒语(津巴布韦)',
'nds': '低地德语',
'nds-NL': '低萨克森语',
'nds-DE': '低地德语(德国)',
'ne': '尼泊尔语',
'ne-NP': '尼泊尔语(尼泊尔)',
'ne-IN': '尼泊尔语(印度)',
'nl': '荷兰语',
'nl-SX': '荷兰语(荷属圣马丁)',
'nl-SR': '荷兰语(苏里南)',
'nl-NL': '荷兰语(荷兰)',
'nl-CW': '荷兰语(库拉索)',
'nl-BQ': '荷兰语(荷属加勒比区)',
'nl-BE': '弗拉芒语',
'nl-AW': '荷兰语(阿鲁巴)',
'nmg': '夸西奥语',
'nmg-CM': '夸西奥语(喀麦隆)',
'nn': '挪威尼诺斯克语',
'nn-NO': '挪威尼诺斯克语(挪威)',
'nnh': '恩甘澎语',
'nnh-CM': '恩甘澎语(喀麦隆)',
'no': '挪威语',
'nus': '努埃尔语',
'nus-SS': '努埃尔语(南苏丹)',
'nyn': '尼昂科勒语',
'nyn-UG': '尼昂科勒语(乌干达)',
'om': '奥罗莫语',
'om-KE': '奥罗莫语(肯尼亚)',
'om-ET': '奥罗莫语(埃塞俄比亚)',
'or': '奥里亚语',
'or-IN': '奥里亚语(印度)',
'os': '奥塞梯语',
'os-RU': '奥塞梯语(俄罗斯)',
'os-GE': '奥塞梯语(格鲁吉亚)',
'pa': '旁遮普语',
'pa-Guru-IN': '旁遮普语(果鲁穆奇文,印度)',
'pa-Guru': '旁遮普语(果鲁穆奇文)',
'pa-Arab-PK': '旁遮普语(阿拉伯文,巴基斯坦)',
'pa-Arab': '旁遮普语(阿拉伯文)',
'pcm': '尼日利亚皮钦语',
'pcm-NG': '尼日利亚皮钦语(尼日利亚)',
'pl': '波兰语',
'pl-PL': '波兰语(波兰)',
'ps': '普什图语',
'ps-PK': '普什图语(巴基斯坦)',
'ps-AF': '普什图语(阿富汗)',
'pt': '葡萄牙语',
'pt-TL': '葡萄牙语(东帝汶)',
'pt-ST': '葡萄牙语(圣多美和普林西比)',
'pt-PT': '欧洲葡萄牙语',
'pt-MZ': '葡萄牙语(莫桑比克)',
'pt-MO': '葡萄牙语(中国澳门特别行政区)',
'pt-LU': '葡萄牙语(卢森堡)',
'pt-GW': '葡萄牙语(几内亚比绍)',
'pt-GQ': '葡萄牙语(赤道几内亚)',
'pt-CV': '葡萄牙语(佛得角)',
'pt-CH': '葡萄牙语(瑞士)',
'pt-BR': '巴西葡萄牙语',
'pt-AO': '葡萄牙语(安哥拉)',
'qu': '克丘亚语',
'qu-PE': '克丘亚语(秘鲁)',
'qu-EC': '克丘亚语(厄瓜多尔)',
'qu-BO': '克丘亚语(玻利维亚)',
'rm': '罗曼什语',
'rm-CH': '罗曼什语(瑞士)',
'rn': '隆迪语',
'rn-BI': '隆迪语(布隆迪)',
'ro': '罗马尼亚语',
'ro-RO': '罗马尼亚语(罗马尼亚)',
'ro-MD': '摩尔多瓦语',
'rof': '兰博语',
'rof-TZ': '兰博语(坦桑尼亚)',
'ru': '俄语',
'ru-UA': '俄语(乌克兰)',
'ru-RU': '俄语(俄罗斯)',
'ru-MD': '俄语(摩尔多瓦)',
'ru-KZ': '俄语(哈萨克斯坦)',
'ru-KG': '俄语(吉尔吉斯斯坦)',
'ru-BY': '俄语(白俄罗斯)',
'rw': '卢旺达语',
'rw-RW': '卢旺达语(卢旺达)',
'rwk': '罗瓦语',
'rwk-TZ': '罗瓦语(坦桑尼亚)',
'sa': '梵语',
'sa-IN': '梵语(印度)',
'sah': '萨哈语',
'sah-RU': '萨哈语(俄罗斯)',
'saq': '桑布鲁语',
'saq-KE': '桑布鲁语(肯尼亚)',
'sat': '桑塔利语',
'sat-Olck-IN': '桑塔利语(桑塔利文,印度)',
'sat-Olck': '桑塔利语(桑塔利文)',
'sbp': '桑古语',
'sbp-TZ': '桑古语(坦桑尼亚)',
'sc': '萨丁语',
'sc-IT': '萨丁语(意大利)',
'sd': '信德语',
'sd-Deva-IN': '信德语(天城文,印度)',
'sd-Deva': '信德语(天城文)',
'sd-Arab-PK': '信德语(阿拉伯文,巴基斯坦)',
'sd-Arab': '信德语(阿拉伯文)',
'se': '北方萨米语',
'se-SE': '北方萨米语(瑞典)',
'se-NO': '北方萨米语(挪威)',
'se-FI': '北方萨米语(芬兰)',
'seh': '塞纳语',
'seh-MZ': '塞纳语(莫桑比克)',
'ses': '东桑海语',
'ses-ML': '东桑海语(马里)',
'sg': '桑戈语',
'sg-CF': '桑戈语(中非共和国)',
'shi': '希尔哈语',
'shi-Tfng-MA': '希尔哈语(提非纳文,摩洛哥)',
'shi-Tfng': '希尔哈语(提非纳文)',
'shi-Latn-MA': '希尔哈语(拉丁文,摩洛哥)',
'shi-Latn': '希尔哈语(拉丁文)',
'si': '僧伽罗语',
'si-LK': '僧伽罗语(斯里兰卡)',
'sk': '斯洛伐克语',
'sk-SK': '斯洛伐克语(斯洛伐克)',
'sl': '斯洛文尼亚语',
'sl-SI': '斯洛文尼亚语(斯洛文尼亚)',
'smn': '伊纳里萨米语',
'smn-FI': '伊纳里萨米语(芬兰)',
'sn': '绍纳语',
'sn-ZW': '绍纳语(津巴布韦)',
'so': '索马里语',
'so-SO': '索马里语(索马里)',
'so-KE': '索马里语(肯尼亚)',
'so-ET': '索马里语(埃塞俄比亚)',
'so-DJ': '索马里语(吉布提)',
'sq': '阿尔巴尼亚语',
'sq-XK': '阿尔巴尼亚语(科索沃)',
'sq-MK': '阿尔巴尼亚语(北马其顿)',
'sq-AL': '阿尔巴尼亚语(阿尔巴尼亚)',
'sr': '塞尔维亚语',
'sr-Latn-XK': '塞尔维亚语(拉丁文,科索沃)',
'sr-Latn-RS': '塞尔维亚语(拉丁文,塞尔维亚)',
'sr-Latn-ME': '塞尔维亚语(拉丁文,黑山)',
'sr-Latn-BA': '塞尔维亚语(拉丁文,波斯尼亚和黑塞哥维那)',
'sr-Latn': '塞尔维亚语(拉丁文)',
'sr-Cyrl-XK': '塞尔维亚语(西里尔文,科索沃)',
'sr-Cyrl-RS': '塞尔维亚语(西里尔文,塞尔维亚)',
'sr-Cyrl-ME': '塞尔维亚语(西里尔文,黑山)',
'sr-Cyrl-BA': '塞尔维亚语(西里尔文,波斯尼亚和黑塞哥维那)',
'sr-Cyrl': '塞尔维亚语(西里尔文)',
'su': '巽他语',
'su-Latn-ID': '巽他语(拉丁文,印度尼西亚)',
'su-Latn': '巽他语(拉丁文)',
'sv': '瑞典语',
'sv-SE': '瑞典语(瑞典)',
'sv-FI': '瑞典语(芬兰)',
'sv-AX': '瑞典语(奥兰群岛)',
'sw': '斯瓦希里语',
'sw-UG': '斯瓦希里语(乌干达)',
'sw-TZ': '斯瓦希里语(坦桑尼亚)',
'sw-KE': '斯瓦希里语(肯尼亚)',
'sw-CD': '刚果斯瓦希里语',
'ta': '泰米尔语',
'ta-SG': '泰米尔语(新加坡)',
'ta-MY': '泰米尔语(马来西亚)',
'ta-LK': '泰米尔语(斯里兰卡)',
'ta-IN': '泰米尔语(印度)',
'te': '泰卢固语',
'te-IN': '泰卢固语(印度)',
'teo': '特索语',
'teo-UG': '特索语(乌干达)',
'teo-KE': '特索语(肯尼亚)',
'tg': '塔吉克语',
'tg-TJ': '塔吉克语(塔吉克斯坦)',
'th': '泰语',
'th-TH': '泰语(泰国)',
'ti': '提格利尼亚语',
'ti-ET': '提格利尼亚语(埃塞俄比亚)',
'ti-ER': '提格利尼亚语(厄立特里亚)',
'tk': '土库曼语',
'tk-TM': '土库曼语(土库曼斯坦)',
'to': '汤加语',
'to-TO': '汤加语(汤加)',
'tr': '土耳其语',
'tr-TR': '土耳其语(土耳其)',
'tr-CY': '土耳其语(塞浦路斯)',
'tt': '鞑靼语',
'tt-RU': '鞑靼语(俄罗斯)',
'twq': '北桑海语',
'twq-NE': '北桑海语(尼日尔)',
'tzm': '塔马齐格特语',
'tzm-MA': '塔马齐格特语(摩洛哥)',
'ug': '维吾尔语',
'ug-CN': '维吾尔语(中国)',
'uk': '乌克兰语',
'uk-UA': '乌克兰语(乌克兰)',
'ur': '乌尔都语',
'ur-PK': '乌尔都语(巴基斯坦)',
'ur-IN': '乌尔都语(印度)',
'uz': '乌兹别克语',
'uz-Latn-UZ': '乌兹别克语(拉丁文,乌兹别克斯坦)',
'uz-Latn': '乌兹别克语(拉丁文)',
'uz-Cyrl-UZ': '乌兹别克语(西里尔文,乌兹别克斯坦)',
'uz-Cyrl': '乌兹别克语(西里尔文)',
'uz-Arab-AF': '乌兹别克语(阿拉伯文,阿富汗)',
'uz-Arab': '乌兹别克语(阿拉伯文)',
'vai': '瓦伊语',
'vai-Vaii-LR': '瓦伊语(瓦依文,利比里亚)',
'vai-Vaii': '瓦伊语(瓦依文)',
'vai-Latn-LR': '瓦伊语(拉丁文,利比里亚)',
'vai-Latn': '瓦伊语(拉丁文)',
'vi': '越南语',
'vi-VN': '越南语(越南)',
'vun': '温旧语',
'vun-TZ': '温旧语(坦桑尼亚)',
'wae': '瓦尔瑟语',
'wae-CH': '瓦尔瑟语(瑞士)',
'wo': '沃洛夫语',
'wo-SN': '沃洛夫语(塞内加尔)',
'xh': '科萨语',
'xh-ZA': '科萨语(南非)',
'xog': '索加语',
'xog-UG': '索加语(乌干达)',
'yav': '洋卞语',
'yav-CM': '洋卞语(喀麦隆)',
'yi': '意第绪语',
'yi-001': '意第绪语(世界)',
'yo': '约鲁巴语',
'yo-NG': '约鲁巴语(尼日利亚)',
'yo-BJ': '约鲁巴语(贝宁)',
'yrl': 'yrl',
'yrl-VE': 'yrl委内瑞拉',
'yrl-CO': 'yrl哥伦比亚',
'yrl-BR': 'yrl巴西',
'zgh': '标准摩洛哥塔马塞特语',
'zgh-MA': '标准摩洛哥塔马塞特语(摩洛哥)',
'zh': '中文',
'zh-Hans-CN': '简体中文(中国)',
'zh-Hans-HK': '简体中文(中国香港特别行政区)',
'zh-Hans-MO': '简体中文(中国澳门特别行政区)',
'zh-Hans-SG': '简体中文(新加坡)',
'zh-Hans': '简体中文',
'zh-Hant-TW': '繁体中文(中国台湾)',
'zh-Hant-MO': '繁体中文(中国澳门特别行政区)',
'zh-Hant-HK': '繁体中文(中国香港特别行政区)',
'zh-Hant': '繁体中文',
'zu': '祖鲁语',
'zu-ZA': '祖鲁语(南非)',
},
error: {
'editor_dirty': '当前检测到未保存的内容,请先保存项目',
'translate_api_error': '翻译请求错误,请反馈给对应平台',
'unknown_file_type': '未知文件类型: {}',
'target_translate_data_not_found': '未找到目标语言: {}',
'target_translate_data_exist': '目标语言已存在: {}',
'local_language_not_set': '没有本地开发语言集',
'translate_provider_config_not_found': '翻译人员证书未设置',
'provider_tag_not_found': '请先选择一种服务商识别语言',
'translate_item_not_found': '没有发现翻译数据: {}',
'scan_option_empty': '扫描选项为空',
'provider_input_error': '{}',
'auto_translate_network_error': '网络错误: {}',
'po_file_language_not_equal': '导入的PO文件的 [Language] 不等于目标语言',
'unavailable_csv_file': 'CSV文件不可用 {}',
'unavailable_xlsx_file': 'XLSX文件不可用 {}',
'unknown_error': '未知错误',
'merge_different_key': '尝试用两个不同的键合并数据',
'index_translate_data_not_found': '数据损坏,没有找到 localization-editor/index.yaml',
'scene_error': '未知场景错误,请尝试重新导入插件资源或重新启动编辑器',
'invalid_translate_data': '无效的翻译数据: {}',
'invalid_translate_file_content': '无效的翻译文件内容: {}',
"YOUDAO": {
'error': 'YouDao 报错: {errorCode}, {message}',
'errorCode': 'YouDao 报错码: {errorCode}, 请查看 https://ai.youdao.com/DOCSIRMA/html/%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E7%BF%BB%E8%AF%91/API%E6%96%87%E6%A1%A3/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1/%E6%96%87%E6%9C%AC%E7%BF%BB%E8%AF%91%E6%9C%8D%E5%8A%A1-API%E6%96%87%E6%A1%A3.html 中 [错误代码列表] 对应的信息',
'207': '重放请求',
'301': '辞典查询失败',
'302': '翻译查询失败',
'1412': '超过最大识别字节数',
'2004': '合成字符过长',
'2412': '超过最大请求字符数',
'3412': '超过最大请求字符数',
}
},
};

View File

@@ -0,0 +1,356 @@
{
"package_version": 2,
"version": "1.0.4",
"name": "localization-editor",
"description": "i18n:localization-editor.description",
"main": "./webpack-dist/electron-main/main.js",
"author": "Cocos Creator",
"editor": ">=3.8.5",
"scripts": {
"preinstall": "cd runtime-node-modules && npm install && cd ..",
"build": "npx webpack --node-env production",
"build:dev": "npx webpack --node-env development",
"build:prod": "npx webpack --node-env production",
"dev": "npx webpack -w",
"pack": "npx ts-node ./bin/pack.ts",
"test": "npx jest",
"generate-icu-option": "npx ts-node ./bin/generate-icu-option.ts",
"generate-main-ipc": "npx ts-node ./bin/generate-main-ipc.ts"
},
"dependencies": {
"i18next": "^21.6.16",
"intl-pluralrules": "^1.3.1"
},
"devDependencies": {
"@cocos/creator-types": "^3.8.1",
"@electron/remote": "^2.1.2",
"@jest/types": "^28.1.3",
"@swc/core": "^1.2.223",
"@swc/jest": "^0.2.22",
"@types/adm-zip": "^0.5.0",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.13",
"@types/fs-extra": "^9.0.5",
"@types/glob": "7.2.0",
"@types/jest": "^28.1.3",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "^4.14.182",
"@types/webpack-node-externals": "^2.5.3",
"adm-zip": "^0.5.9",
"axios": "^0.27.2",
"crypto-js": "^4.1.1",
"csv": "^6.2.0",
"express": "^4.18.1",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"gettext-extractor": "^3.5.4",
"gettext-parser": "^5.1.2",
"glob": "7.2.0",
"jest": "^28.1.3",
"js-yaml": "^4.1.0",
"less": "^4.1.2",
"less-loader": "^11.0.0",
"raw-loader": "^4.0.2",
"reflect-metadata": "^0.1.13",
"regenerator-runtime": "^0.13.9",
"swc-loader": "^0.2.3",
"ts-jest": "^28.0.5",
"ts-morph": "^15.1.0",
"ts-node": "^10.7.0",
"tsyringe": "^4.6.0",
"typescript": "^4.6.2",
"vue": "^3.2.25",
"vue-loader": "^17.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dashboard": "^3.3.7",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0",
"xlsx": "^0.18.5",
"yaml": "^2.1.3"
},
"panels": {
"default": {
"title": "Localization Editor",
"type": "dockable",
"main": "webpack-dist/electron-renderer/default.js",
"icon": "./static/icon-2x.png",
"size": {
"min-width": 1000,
"min-height": 750,
"width": 1280,
"height": 1200
}
}
},
"contributions": {
"inspector": {
"section": {
"node": {
"L10nLabel": "webpack-dist/electron-renderer/l10n-label-inspector.js"
}
}
},
"menu": [
{
"path": "i18n:menu.panel",
"label": "i18n:localization-editor.localization-editor",
"icon": "./static/icon.png",
"message": "open-panel"
}
],
"messages": {
"execute-panel-method": {
"methods": [
"default.executePanelMethod"
]
},
"scene:ready": {
"methods": [
"onSceneReady"
]
},
"builder:task-changed": {
"methods": [
"onBuilderTaskChanged"
]
},
"toggle": {
"methods": [
"toggle"
]
},
"set-dirty": {
"methods": [
"setDirty"
]
},
"get-dirty": {
"methods": [
"getDirty"
]
},
"enable-changed": {
"methods": [
"enableChanged"
]
},
"get-enable": {
"methods": [
"getEnable"
]
},
"open-panel": {
"methods": [
"openPanel"
]
},
"close-panel": {
"methods": [
"closePanel"
]
},
"preview-by": {
"methods": [
"previewBy"
]
},
"scan": {
"methods": [
"scan"
]
},
"uninstall": {
"methods": [
"uninstall"
]
},
"read-config": {
"methods": [
"readConfig"
]
},
"get-index-data": {
"methods": [
"getIndexData"
]
},
"get-local-language": {
"methods": [
"getLocalLanguage"
]
},
"get-translate-data": {
"methods": [
"getTranslateData"
]
},
"get-translate-data-object": {
"methods": [
"getTranslateDataObject"
]
},
"save-translate-data": {
"methods": [
"saveTranslateData"
]
},
"clear-translate-data": {
"methods": [
"clearTranslateData"
]
},
"set-local-language-locale": {
"methods": [
"setLocalLanguageLocale"
]
},
"set-language-config": {
"methods": [
"setLanguageConfig"
]
},
"get-language-config": {
"methods": [
"getLanguageConfig"
]
},
"get-all-language-configs": {
"methods": [
"getAllLanguageConfigs"
]
},
"add-target-language": {
"methods": [
"addTargetLanguage"
]
},
"remove-target-language": {
"methods": [
"removeTargetLanguage"
]
},
"get-translate-providers": {
"methods": [
"getTranslateProviders"
]
},
"get-translate-provider-supported-languages": {
"methods": [
"getTranslateProviderSupportedLanguages"
]
},
"get-current-translate-provider": {
"methods": [
"getCurrentTranslateProvider"
]
},
"get-translate-provider": {
"methods": [
"getTranslateProvider"
]
},
"set-current-translate-provider": {
"methods": [
"setCurrentTranslateProvider"
]
},
"clear-translate-provider": {
"methods": [
"clearTranslateProvider"
]
},
"change-value": {
"methods": [
"changeValue"
]
},
"get-scan-options": {
"methods": [
"getScanOptions"
]
},
"auto-translate": {
"methods": [
"autoTranslate"
]
},
"import-media-files": {
"methods": [
"importMediaFiles"
]
},
"compile": {
"methods": [
"compile"
]
},
"add-association": {
"methods": [
"addAssociation"
]
},
"remove-association": {
"methods": [
"removeAssociation"
]
},
"get-resource-list": {
"methods": [
"getResourceList"
]
},
"get-resource-bundle": {
"methods": [
"getResourceBundle"
]
},
"import-translate-file": {
"methods": [
"importTranslateFile"
]
},
"export-translate-file": {
"methods": [
"exportTranslateFile"
]
}
},
"asset-db": {
"mount": {
"path": "./static/assets",
"readonly": true,
"visible": true,
"enable": "L10nEnable"
}
},
"scene": {
"script": "./webpack-dist/electron-main/scene.js"
},
"profile": {
"project": {
"ProjectConfig": {
"default": {},
"label": "i18n:localization_editor_project_config"
},
"L10nEnable": {
"default": false,
"label": "i18n:localization_editor_project_config",
"message": "enable-changed"
}
},
"editor": {
"EditorConfig": {
"default": {},
"label": "i18n:localization_editor_editor_config"
},
"TranslateProviderConfigMap": {
"default": {},
"label": "i18n:localization_editor_provider_config"
}
}
},
"builder": "webpack-dist/electron-main/builder.js",
"server": "webpack-dist/electron-main/server.js"
}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "e20945fc-1f04-4216-a044-2652e1ebdf78",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}

View File

@@ -0,0 +1,516 @@
import { _decorator, CCBoolean, CCInteger, CCString, Enum, Label } from 'cc';
// @ts-ignore
import { EDITOR } from 'cc/env';
// import ICUType from '../core/ICUType';
// import I18nComponent from './I18nComponent';
// import { DateTimeFormatOptions, NumberFormatOptions, RelativeTimeFormatOptions, RelativeTimeFormatUnit } from '../core/ICUOptions';
// import intl from '../core/IntlManager';
const {
ccclass,
property,
requireComponent,
executeInEditMode,
} = _decorator;
enum VirtualEnum {}
// @ccclass('ICUComponent')
// @executeInEditMode(true)
// @requireComponent(Label)
// export default class ICUComponent extends I18nComponent {
// @property({ visible: false })
// _icuValue = '';
//
// @property
// set icuValue(value: string) {
// this._icuValue = value;
// this.render();
// }
//
// get icuValue(): string {
// return this._icuValue;
// }
//
// @property({ visible: false })
// _type: ICUType = ICUType.DateTime;
//
// @property({ visible: true, type: Enum(ICUType) })
// set type(value: ICUType) {
// this._type = value;
// }
//
// get type(): ICUType {
// return this._type;
// }
//
// protected onLoad() {
// super.onLoad();
// if (this.label && !this._icuValue) {
// this._icuValue = this.label.string;
// }
// }
//
// protected start() {
// this.render();
// }
//
// public render() {
// super.render();
// if (this._icuValue.length === 0) return;
// let translatedString!: string;
// try {
// switch (this.type) {
// case ICUType.Number:
// translatedString = intl.tn(parseFloat(this.icuValue), this.numberFormatOptions);
// break;
// case ICUType.DateTime:
// translatedString = intl.td(new Date(this.icuValue), this.dateTimeFormatOptions);
// break;
// case ICUType.RelativeTime:
// translatedString = intl.tt(
// parseFloat(this.icuValue),
// this.relativeTimeUnit,
// this.relativeTimeFormatOptions,
// );
// break;
// case ICUType.List: {
// const icuList = this.icuValue.split(',');
// translatedString = intl.tl(icuList);
// break;
// }
// default:
// break;
// }
// } catch (e) {
// translatedString = this._icuValue;
// }
// if (EDITOR) {
// this.preview(translatedString);
// } else {
// this.label!.string = translatedString;
// }
// }
//
// // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ auto generate by script don't edit ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// /************************** NumberFormatOptions **************************/
// @property({ visible: false })
// numberFormatOptions: NumberFormatOptions = { useGrouping: false };
// @property({
// type: Enum(VirtualEnum),
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set numberStyle(value: string) {
// this.numberFormatOptions.style = value;
// this.render();
// }
// get numberStyle(): string {
// return this.numberFormatOptions.style as string;
// }
//
// @property({
// type: CCString,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set currency(value: string) {
// this.numberFormatOptions.currency = value;
// this.render();
// }
// get currency(): string {
// return this.numberFormatOptions.currency as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set currencySign(value: string) {
// this.numberFormatOptions.currencySign = value;
// this.render();
// }
// get currencySign(): string {
// return this.numberFormatOptions.currencySign as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set currencyDisplay(value: string) {
// this.numberFormatOptions.currencyDisplay = value;
// this.render();
// }
// get currencyDisplay(): string {
// return this.numberFormatOptions.currencyDisplay as string;
// }
//
// @property({
// type: CCBoolean,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set useGrouping(value: boolean) {
// this.numberFormatOptions.useGrouping = value;
// this.render();
// }
// get useGrouping(): boolean {
// return this.numberFormatOptions.useGrouping as boolean;
// }
//
// @property({
// type: CCInteger,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set minimumIntegerDigits(value: number) {
// this.numberFormatOptions.minimumIntegerDigits = value;
// this.render();
// }
// get minimumIntegerDigits(): number {
// return this.numberFormatOptions.minimumIntegerDigits as number;
// }
//
// @property({
// type: CCInteger,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set minimumFractionDigits(value: number) {
// this.numberFormatOptions.minimumFractionDigits = value;
// this.render();
// }
// get minimumFractionDigits(): number {
// return this.numberFormatOptions.minimumFractionDigits as number;
// }
//
// @property({
// type: CCInteger,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set maximumFractionDigits(value: number) {
// this.numberFormatOptions.maximumFractionDigits = value;
// this.render();
// }
// get maximumFractionDigits(): number {
// return this.numberFormatOptions.maximumFractionDigits as number;
// }
//
// @property({
// type: CCInteger,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set minimumSignificantDigits(value: number) {
// this.numberFormatOptions.minimumSignificantDigits = value;
// this.render();
// }
// get minimumSignificantDigits(): number {
// return this.numberFormatOptions.minimumSignificantDigits as number;
// }
//
// @property({
// type: CCInteger,
// group: 'Number',
// visible(this: ICUComponent) {
// return this.type === ICUType.Number;
// },
// })
// set maximumSignificantDigits(value: number) {
// this.numberFormatOptions.maximumSignificantDigits = value;
// this.render();
// }
// get maximumSignificantDigits(): number {
// return this.numberFormatOptions.maximumSignificantDigits as number;
// }
//
// /************************** DateTimeFormatOptions **************************/
// @property({ visible: false })
// dateTimeFormatOptions: DateTimeFormatOptions = {};
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set dateTimeLocaleMatcher(value: string) {
// this.dateTimeFormatOptions.localeMatcher = value;
// this.render();
// }
// get dateTimeLocaleMatcher(): string {
// return this.dateTimeFormatOptions.localeMatcher as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set weekday(value: string) {
// this.dateTimeFormatOptions.weekday = value;
// this.render();
// }
// get weekday(): string {
// return this.dateTimeFormatOptions.weekday as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set era(value: string) {
// this.dateTimeFormatOptions.era = value;
// this.render();
// }
// get era(): string {
// return this.dateTimeFormatOptions.era as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set year(value: string) {
// this.dateTimeFormatOptions.year = value;
// this.render();
// }
// get year(): string {
// return this.dateTimeFormatOptions.year as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set month(value: string) {
// this.dateTimeFormatOptions.month = value;
// this.render();
// }
// get month(): string {
// return this.dateTimeFormatOptions.month as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set day(value: string) {
// this.dateTimeFormatOptions.day = value;
// this.render();
// }
// get day(): string {
// return this.dateTimeFormatOptions.day as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set hour(value: string) {
// this.dateTimeFormatOptions.hour = value;
// this.render();
// }
// get hour(): string {
// return this.dateTimeFormatOptions.hour as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set minute(value: string) {
// this.dateTimeFormatOptions.minute = value;
// this.render();
// }
// get minute(): string {
// return this.dateTimeFormatOptions.minute as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set second(value: string) {
// this.dateTimeFormatOptions.second = value;
// this.render();
// }
// get second(): string {
// return this.dateTimeFormatOptions.second as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set timeZoneName(value: string) {
// this.dateTimeFormatOptions.timeZoneName = value;
// this.render();
// }
// get timeZoneName(): string {
// return this.dateTimeFormatOptions.timeZoneName as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set formatMatcher(value: string) {
// this.dateTimeFormatOptions.formatMatcher = value;
// this.render();
// }
// get formatMatcher(): string {
// return this.dateTimeFormatOptions.formatMatcher as string;
// }
//
// @property({
// type: CCBoolean,
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set hour12(value: boolean) {
// this.dateTimeFormatOptions.hour12 = value;
// this.render();
// }
// get hour12(): boolean {
// return this.dateTimeFormatOptions.hour12 as boolean;
// }
//
// @property({
// type: CCString,
// group: 'DateTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.DateTime;
// },
// })
// set timeZone(value: string) {
// this.dateTimeFormatOptions.timeZone = value;
// this.render();
// }
// get timeZone(): string {
// return this.dateTimeFormatOptions.timeZone as string;
// }
//
// /************************** RelativeTimeOptions **************************/
// @property({ visible: false })
// relativeTimeFormatOptions: RelativeTimeFormatOptions = {};
//
// @property({
// type: Enum(VirtualEnum),
// group: 'RelativeTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.RelativeTime;
// },
// })
// set relativeTimeLocaleMatcher(value: string) {
// this.relativeTimeFormatOptions.localeMatcher = value;
// this.render();
// }
// get relativeTimeLocaleMatcher(): string {
// return this.relativeTimeFormatOptions.localeMatcher as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'RelativeTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.RelativeTime;
// },
// })
// set numeric(value: string) {
// this.relativeTimeFormatOptions.numeric = value;
// this.render();
// }
// get numeric(): string {
// return this.relativeTimeFormatOptions.numeric as string;
// }
//
// @property({
// type: Enum(VirtualEnum),
// group: 'RelativeTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.RelativeTime;
// },
// })
// set relativeTimeStyle(value: string) {
// this.relativeTimeFormatOptions.style = value;
// this.render();
// }
// get relativeTimeStyle(): string {
// return this.relativeTimeFormatOptions.style as string;
// }
//
// @property({ visible: false })
// _relativeTimeUnit: RelativeTimeFormatUnit = 'second';
//
// @property({
// type: Enum(VirtualEnum),
// group: 'RelativeTime',
// visible(this: ICUComponent) {
// return this.type === ICUType.RelativeTime;
// },
// })
// set relativeTimeUnit(value: string) {
// this._relativeTimeUnit = value;
// this.render();
// }
// get relativeTimeUnit(): string {
// return this._relativeTimeUnit;
// }
//
// // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ auto generate by script don't edit ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// }

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "7a970d99-7d42-4b01-895c-a1fd06c411b5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,53 @@
import { _decorator, Component, Label, Node } from 'cc';
// @ts-ignore
import { EDITOR } from 'cc/env';
const {
ccclass,
property,
disallowMultiple,
requireComponent,
menu,
} = _decorator;
@ccclass('L10nComponent')
@requireComponent(Label)
@disallowMultiple()
@menu('hidden:LocalizationEditor/L10nComponent')
export default abstract class L10nComponent extends Component {
protected constructor() {
super();
}
@property({
readonly: true,
tooltip: 'i18n:localization-editor.component.string'
})
get string() {
return this.label?.string || '';
}
label?: Label | null = undefined;
protected onLoad() {
this.label = this.node.getComponent(Label);
}
protected start() {
this.render();
}
public render() {}
public preview(value: string) {
if (this.label && EDITOR) {
const originalString = this.label.string;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.label._string = value;
this.label.updateRenderData(true);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
cce.Engine.repaintInEditMode();
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "64a567b7-70a8-4921-894e-b2b550a6ba6f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,66 @@
// @ts-ignore
import { _decorator } from 'cc';
// @ts-ignore
import { EDITOR } from 'cc/env';
import l10n from '../core/l10n-manager';
import L10nComponent from './l10n-component';
const {
ccclass,
property,
executeInEditMode,
menu,
help,
} = _decorator;
@ccclass('L10nLabel')
@executeInEditMode(true)
@menu('LocalizationEditor/L10nLabel')
@help('i18n:localization-editor.component.help')
export default class L10nLabel extends L10nComponent {
@property({ visible: false })
_key = '';
@property({
tooltip: 'i18n:localization-editor.component.key1'
})
set key(value: string) {
this._key = value;
this.render();
}
get key(): string {
return this._key;
}
@property({ visible: false })
_count = 0;
@property({
tooltip: 'i18n:localization-editor.component.count'
})
set count(value: number) {
this._count = value;
this.render();
}
get count(): number {
return this._count;
}
onLoad() {
if (typeof super.onLoad === 'function') {
super.onLoad();
}
}
render() {
const translatedString = l10n.t(this._key, { count: this._count });
if (typeof this.label === 'undefined' || typeof translatedString === 'undefined') {
return;
}
if (EDITOR) {
this.preview(translatedString);
} else {
this.label!.string = translatedString;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "30d83ab3-45f6-472d-8305-9f8280f4f1b8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,13 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "ef8d37e0-634c-4106-a3ee-13df564c46a3",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {},
"bundleName": "1"
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "8726de39-9138-4457-8098-f4b669c27882",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,14 @@
import l10n from './l10n-manager';
import { game, cclegacy } from 'cc';
// @ts-expect-error
import { EDITOR } from 'cc/env';
if (cclegacy.GAME_VIEW || EDITOR) { // for Editor
// @ts-expect-error we need top level await in Editor
await l10n.createIntl({});
} else { // for Runtime or Preview
game.onPostProjectInitDelegate.add(
() => l10n.createIntl({}),
);
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "2f21bc78-8001-4d8f-a75c-b7da831c91c7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,65 @@
import { FallbackLanguage, L10nValue } from './l10n-options';
export type FormattedValue = string;
export type TextInfoDirection = 'ltr' | 'rtl';
export interface StandardOption {
count?: number;
// 暂不开放
// context?: string
defaultValue?: L10nValue;
// returnObjects?: boolean;
language?: Intl.BCP47LanguageTag;
fallbackLanguage?: FallbackLanguage;
// 暂不开放
// joinArrays?: string
}
export interface Template {
[key: string]:
| string
| {
[key: string]: StandardOption;
};
}
export interface NumberFormatOptions extends Intl.NumberFormatOptions {
style?: 'decimal' | 'percent' | 'currency' | string;
/**
* 货币代码采用ISO 4217标准
* @see ISO4217Tag
*/
currency?: string;
currencySign?: 'standard' | 'accounting' | string;
currencyDisplay?: 'symbol' | 'code' | 'name' | string;
useGrouping?: boolean;
minimumIntegerDigits?: number;
minimumFractionDigits?: number;
maximumFractionDigits?: number;
minimumSignificantDigits?: number;
maximumSignificantDigits?: number;
}
export interface DateTimeFormatOptions {
localeMatcher?: 'best fit' | 'lookup' | undefined | string;
weekday?: 'long' | 'short' | 'narrow' | undefined | string;
era?: 'long' | 'short' | 'narrow' | undefined | string;
year?: 'numeric' | '2-digit' | undefined | string;
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' | undefined | string;
day?: 'numeric' | '2-digit' | undefined | string;
hour?: 'numeric' | '2-digit' | undefined | string;
minute?: 'numeric' | '2-digit' | undefined | string;
second?: 'numeric' | '2-digit' | undefined | string;
timeZoneName?: 'long' | 'short' | undefined | string;
formatMatcher?: 'best fit' | 'basic' | undefined | string;
hour12?: boolean | undefined;
timeZone?: string | undefined;
}
export type RelativeTimeFormatUnit = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' | string;
export interface RelativeTimeFormatOptions {
localeMatcher?: 'lookup' | 'best fit' | string;
style?: 'narrow' | 'short' | 'long' | string;
numeric?: 'auto' | 'always' | string;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "dc153188-b6f7-4234-8025-20163cbd0f8c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,11 @@
/**
* Intl formatting
*/
enum ICUType {
DateTime,
Number,
List,
RelativeTime,
}
export default ICUType;

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "e5dc4963-9221-45a4-8f4b-a8af210423e5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,12 @@
enum L10nListenEvent {
languageChanged = 'languageChanged',
onMissingKey = 'missingKey',
/**
* store events
*/
// onAdded = 'added',
// onRemoved = 'removed',
}
export default L10nListenEvent;

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "3415306a-cc9f-4b47-8961-2437afa40e1f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,255 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { createInstance, i18n, InitOptions as I18NextInitOptions } from 'i18next';
// @ts-ignore
import { EDITOR, BUILD, PREVIEW } from 'cc/env';
// @ts-ignore
import { game, assetManager } from 'cc';
import type { L10nOptions, ResourceData, L10nKey, L10nValue, ResourceItem } from './l10n-options';
import {
DateTimeFormatOptions,
FormattedValue,
NumberFormatOptions,
RelativeTimeFormatOptions,
RelativeTimeFormatUnit,
StandardOption,
Template,
TextInfoDirection,
} from './icu-options';
import L10nListenEvent from './l10n-listen-event';
import ResourceDataManager from './resource-data-manager';
import { mainName, pluginName } from './localization-global';
import { ResourceBundle, ResourceList } from './l10n-options';
export class L10nManager {
static LOCAL_STORAGE_LANGUAGE_KEY = `${mainName}/language`;
static readonly DEFAULT_NAMESPACE = 'translation' as const;
static readonly ASSET_NAMESPACE = 'asset' as const;
static readonly ALLOW_NAMESPACE = [L10nManager.DEFAULT_NAMESPACE, L10nManager.ASSET_NAMESPACE] as const;
static l10n: L10nManager = new L10nManager();
/**
* @zh
* i18n 实例
* @en
* i18next instance
*/
private _intl?: i18n = undefined;
private _options: L10nOptions = {};
private resourceList?: ResourceList;
private resourceBundle: ResourceBundle = {};
public resourceDataManager: ResourceDataManager;
private constructor() {
this.resourceDataManager = new ResourceDataManager();
}
public isInitialized(): boolean {
return this._intl?.isInitialized ?? false;
}
public async createIntl(options: L10nOptions) {
const reloadResult = await this.reloadResourceData();
if (!reloadResult) {
return;
}
this._options = options;
this._intl = createInstance();
let localStorageLanguage: string | undefined = undefined;
if (BUILD && !PREVIEW) {
localStorageLanguage = localStorage.getItem(
l10n['_options'].localStorageLanguageKey ?? L10nManager.LOCAL_STORAGE_LANGUAGE_KEY,
);
localStorageLanguage = this.checkLanguage(localStorageLanguage);
}
const defaultLanguage = localStorageLanguage ?? options.language ?? this.resourceList.defaultLanguage;
const fallbackLanguage = options.fallbackLanguage ?? this.resourceList.fallbackLanguage;
const resources = options.resources ?? this.resourceBundle;
const i18nextOptions: I18NextInitOptions = {
lng: defaultLanguage,
fallbackLng: fallbackLanguage,
resources: { ...resources },
ns: L10nManager.ALLOW_NAMESPACE,
defaultNS: L10nManager.DEFAULT_NAMESPACE,
initImmediate: false,
load: 'currentOnly',
};
await this._intl.init(i18nextOptions);
this.setAssetOverrideMap(resources[defaultLanguage][L10nManager.ASSET_NAMESPACE]);
}
public checkLanguage(language: string): string | undefined {
if (!language || language.length === 0 || language === 'null' || language === null || language === 'undefined' || language === undefined) {
return undefined;
}
if (this.resourceList && this.resourceList.languages.length > 0 && this.resourceList.languages.find(it => it === language)) {
return language;
}
return undefined;
}
public cloneIntl(options: L10nOptions) {
if (!this._intl) {
throw new Error('i18next not init, please use \'l10n.createIntl\'');
}
this._intl = this._intl.cloneInstance(options);
}
async reloadResourceData(): Promise<boolean> {
this.resourceList = await this.resourceDataManager.readResourceList();
if (!this.resourceList) {
console.log(`[${pluginName}] not found translate language list, skip init l10n`);
return false;
}
this.resourceBundle = await this.resourceDataManager.readResourceBundle(this.resourceList?.languages ?? []);
if (!this.resourceList?.defaultLanguage) {
console.log(`[${pluginName}] not found translate language data, skip init l10n`);
return false;
}
return true;
}
/** 初始化 i18next */
public config(options: L10nOptions) {
this.cloneIntl(options);
}
public async changeLanguage(language: Intl.BCP47LanguageTag) {
if (!language) {
console.warn(`[${pluginName}] invalid language tag`);
return;
}
console.log(`[${pluginName}] will change language to`, language);
if (this._intl) {
if (this.currentLanguage) {
this.releaseOverrideMap();
}
await this._intl.changeLanguage(language);
this.setAssetOverrideMap(this.resourceBundle[language][L10nManager.ASSET_NAMESPACE]);
if (!EDITOR) {
localStorage.setItem(L10nManager.LOCAL_STORAGE_LANGUAGE_KEY, language);
console.log(`[${pluginName}] game will restart`);
game.restart();
}
} else {
console.log(`[${pluginName}] language data not load, please confirm whether the language data is included in the build`);
}
}
public t(key: L10nKey, options?: StandardOption | Template): L10nValue {
if (!(this._intl?.isInitialized ?? false)) return key;
return this._intl!.t(key, options);
}
/**
* 实验性功能暂不开放
* 数字类ICU
*/
private tn(value: number, options?: NumberFormatOptions): FormattedValue {
if (!(this._intl?.isInitialized ?? false)) return value.toString();
const cloneOptions: NumberFormatOptions = {};
Object.assign(cloneOptions, options);
type NumberFormatOptionsKey = keyof NumberFormatOptions;
for (const key of Object.keys(cloneOptions) as NumberFormatOptionsKey[]) {
if (typeof cloneOptions[key] === 'string' && (cloneOptions[key] as string)!.length === 0) {
delete cloneOptions[key];
} else if (typeof cloneOptions[key] === 'number' && cloneOptions[key] === 0) {
delete cloneOptions[key];
}
}
return new Intl.NumberFormat(this._intl?.language, cloneOptions).format(value);
}
/**
* 实验性功能暂不开放
* 日期/时刻类ICU
*/
private td(date: Date, options?: DateTimeFormatOptions): FormattedValue {
if (!(this._intl?.isInitialized ?? false)) return date.toString();
const cloneOptions: DateTimeFormatOptions = {};
Object.assign(cloneOptions, options);
type DateTimeFormatOptionsKey = keyof DateTimeFormatOptions;
for (const key of Object.keys(cloneOptions) as DateTimeFormatOptionsKey[]) {
if (typeof cloneOptions[key] === 'string' && (cloneOptions[key] as string).length === 0) {
delete cloneOptions[key];
}
}
return new Intl.DateTimeFormat(this._intl?.language, cloneOptions as Intl.DateTimeFormatOptions).format(date);
}
/**
* 实验性功能暂不开放
* 时长类ICU
*/
private tt(value: number, unit: RelativeTimeFormatUnit, options?: RelativeTimeFormatOptions): FormattedValue {
if (!(this._intl?.isInitialized ?? false)) return value.toString();
const cloneOptions: RelativeTimeFormatOptions = {};
Object.assign(cloneOptions, options);
type RelativeTimeFormatOptionsKey = keyof RelativeTimeFormatOptions;
for (const key of Object.keys(cloneOptions) as RelativeTimeFormatOptionsKey[]) {
if (typeof cloneOptions[key] === 'string' && (cloneOptions[key] as string).length === 0) {
delete cloneOptions[key];
}
}
return new Intl.RelativeTimeFormat(this._intl?.language, cloneOptions as Intl.RelativeTimeFormatOptions).format(
value,
unit as Intl.RelativeTimeFormatUnit,
);
}
/**
* 实验性功能暂不开放
* 数组类ICU
*/
private tl(value: string[]): FormattedValue {
if (!(this._intl?.isInitialized ?? false)) return value.toString();
return new Intl.ListFormat(this._intl?.language).format(value);
}
public exists(key: L10nKey): boolean {
return this._intl?.exists(key) ?? false;
}
get currentLanguage(): Intl.BCP47LanguageTag {
return this._intl?.language ?? '';
}
get languages(): readonly Intl.BCP47LanguageTag[] {
return this.resourceList?.languages ?? [];
}
public direction(language?: Intl.BCP47LanguageTag): TextInfoDirection {
return (language ? new Intl.Locale(language) : new Intl.Locale(this._intl!.language)).textInfo()
.direction as TextInfoDirection;
}
public on(event: L10nListenEvent, callback: (...args: any[]) => void) {
this._intl?.on(event, callback);
}
public off(event: L10nListenEvent, callback: (...args: any[]) => void) {
this._intl?.off(event, callback);
}
public getResourceBundle(language: string, namespace: typeof L10nManager.ALLOW_NAMESPACE[number]): ResourceData | undefined {
return this._intl?.getResourceBundle(language, namespace);
}
protected setAssetOverrideMap(assetNamespace: Readonly<ResourceItem>) {
for (const key of Object.keys(assetNamespace)) {
assetManager.assetsOverrideMap.set(key, assetNamespace[key]);
}
}
protected releaseOverrideMap() {
assetManager.assetsOverrideMap.clear();
}
}
const l10n: L10nManager = L10nManager.l10n;
export default l10n;

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "2856aec6-495f-456d-be77-9ce8a6277dc9",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,116 @@
export type L10nKey = string;
export type L10nValue = string;
export interface ResourceList {
defaultLanguage?: Intl.BCP47LanguageTag;
fallbackLanguage?: Intl.BCP47LanguageTag;
languages: Intl.BCP47LanguageTag[];
}
export interface ResourceBundle {
[language: Intl.BCP47LanguageTag]: ResourceData;
}
export interface ResourceData {
[namespace: string]: ResourceItem;
}
export interface ResourceItem {
[key: string]: any;
}
export interface FallbackLanguageObjectList {
[language: string]: readonly string[];
}
export type FallbackLanguage =
| string
| readonly string[]
| FallbackLanguageObjectList
| ((language: Intl.BCP47LanguageTag) => string | readonly string[] | FallbackLanguageObjectList);
export interface L10nOptions {
/**
* Logs info level to console output. Helps finding issues with loading not working.
* @default false
*/
// debug?: boolean;
/**
* Resources to initialize with (if not using loading or not appending using addResourceBundle)
* @default undefined
*/
resources?: ResourceBundle;
/**
* Language to use (overrides language detection)
*/
language?: Intl.BCP47LanguageTag;
/**
* Language to use if translations in user language are not available.
* @default same as language
*/
fallbackLanguage?: false | FallbackLanguage;
/**
* @default IntlManager.LOCAL_STORAGE_LANGUAGE_KEY
*/
localStorageLanguageKey?: string;
/**
* @zh
* 可以对key进行前置处理返回值应该是处理后的key
*
* @en
* Preprocess the key
*
* @param key
* @return string
* onBeforeProcessHandler
*/
beforeTranslate?: (key: L10nKey) => L10nValue;
/**
* @zh
* 对value进行后置处理返回值应该是处理后的value
*
* @en
* Postprocess the value, return the processed value
*
* @param key
* @param value
* @return string
*/
afterTranslate?: (key: string, value: string) => string;
/**
* Allows null values as valid translation
* @default true
*/
returnNull?: boolean;
/**
* Allows empty string as valid translation
* @default true
*/
returnEmptyString?: boolean;
/**
* Allows objects as valid translation result
* @default false
*/
// returnObjects?: boolean;
/**
* Gets called if object was passed in as key but returnObjects was set to false
* @default noop
*/
// returnedObjectHandler?: (key: string, value: string, options: any) => void;
/**
* Char, eg. '\n' that arrays will be joined by
* @default false
*/
// joinArrays?: false | string;
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "b461821b-b11b-4b2d-b469-d9d42b137108",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,5 @@
export const pluginName = 'Localization Editor';
export const mainName = 'localization-editor';
export const runtimeBundleName = 'l10n';
export const resourceListPath = 'resource-list';
export const resourceBundlePath = 'resource-bundle';

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "02d9eb4a-80d0-46a8-b3e9-bd4ed15f6f68",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,90 @@
// @ts-ignore
import { BUILD, EDITOR } from 'cc/env';
import { AssetManager, assetManager, JsonAsset, settings, Settings } from 'cc';
import { ResourceBundle, ResourceList } from './l10n-options';
import { mainName, pluginName, resourceBundlePath, resourceListPath, runtimeBundleName } from './localization-global';
export default class ResourceDataManager {
async readResourceList(): Promise<ResourceList> {
if (EDITOR) {
return Editor.Message.request(mainName, 'get-resource-list');
} else if (BUILD) {
console.log(`[${pluginName}] this is build env`);
return this.runtimeLoad(resourceListPath);
} else {
return this.previewLoad(resourceListPath);
}
}
async readResourceBundle(tags: Intl.BCP47LanguageTag[]): Promise<ResourceBundle> {
if (EDITOR) {
return this.editorLoad(tags);
} else if (BUILD) {
return this.runtimeLoad(resourceBundlePath);
} else {
return this.previewLoad(resourceBundlePath);
}
}
/**
* 编辑器模式下使用
* @param locales
*/
async editorLoad(locales: Intl.BCP47LanguageTag[]): Promise<ResourceBundle | undefined> {
return Editor.Message.request(mainName, 'get-resource-bundle', locales);
}
/**
* 构建后运行时使用
* @param fileName
*/
async runtimeLoad<T>(fileName: string): Promise<T | undefined> {
const bundle = await this.getBundle(runtimeBundleName);
if (!bundle) return undefined;
const jsonAsset = await this.getResource(bundle, fileName);
if (!jsonAsset || !jsonAsset.json) return undefined;
return jsonAsset.json as any as T;
}
/**
* 浏览器预览使用
* @param urlPath
*/
async previewLoad<T>(urlPath: string): Promise<T | undefined> {
try {
return await (await fetch(`${mainName}/${urlPath}`)).json() as T;
} catch (e) {
return undefined;
}
}
async checkBundle(bundleName: string): Promise<boolean> {
const queryResult: { bundle: string, version: string }[] | null = settings.querySettings<{ bundle: string, version: string }[]>(Settings.Category.ASSETS, 'preloadBundles');
const bundle = queryResult?.find((it) => it.bundle === bundleName);
return !!bundle;
}
async getBundle(bundleName: string): Promise<AssetManager.Bundle | undefined> {
return new Promise(resolve => {
assetManager.loadBundle(bundleName, (error, bundle: AssetManager.Bundle) => {
if (error) {
resolve(undefined);
} else {
resolve(bundle);
}
});
});
}
async getResource(bundle: AssetManager.Bundle, resourceName: string): Promise<JsonAsset | undefined> {
return new Promise(resolve => {
bundle.load(resourceName, (error, asset: JsonAsset) => {
if (error) {
resolve(undefined);
} else {
resolve(asset);
}
});
});
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "0a95ad24-8abb-499c-99cc-9d959245a167",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "5edb8c82-d058-4174-84ae-a6624ae65a90",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true,
"bundleConfigID": "default"
}
}

View File

@@ -0,0 +1,21 @@
import l10n, { L10nManager } from './core/l10n-manager';
import L10nListenEvent from './core/l10n-listen-event';
import L10nLabel from './components/l10n-label';
export type {
L10nKey,
L10nValue,
ResourceList,
ResourceBundle,
ResourceData,
ResourceItem,
FallbackLanguageObjectList,
FallbackLanguage,
L10nOptions,
} from './core/l10n-options';
export {
l10n,
L10nManager,
L10nLabel,
L10nListenEvent,
};

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "07b549ce-14f5-4964-9fc4-366b956df8d8",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "2.0.0",
"importer": "json",
"imported": true,
"uuid": "fa0fa81e-eaf8-4eb4-8a48-181676ceff01",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,11 @@
{
"ver": "2.0.0",
"importer": "json",
"imported": true,
"uuid": "d2f18e87-d231-4e20-9e54-06a7aa2bd5c1",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,12 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "43fd725b-630f-4c60-a864-7cea14073726",
"files": [],
"subMetas": {},
"userData": {
"compressionType": {},
"isRemoteBundle": {}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "e9a803d0-7f83-426e-b187-6d1caa791dc6",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "495ecfe5-9e27-4b3e-a048-df0d67da1f21",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "9ff49c15-8e75-44e3-955c-3d31f42d783d",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "3d4a4001-ed8e-4131-b245-b6b580406771",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "60bb8e28-ea3e-45ee-8314-911ce01dc8b0",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1 @@
import 'intl-pluralrules';

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "c959445d-20e1-483c-8aa7-08ca07d74dd4",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "1d313b84-d501-43d7-8173-4315142c25d3",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.23",
"importer": "typescript",
"imported": true,
"uuid": "306c90e6-641a-46df-8383-07e477f49b91",
"files": [],
"subMetas": {},
"userData": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

View File

@@ -0,0 +1,904 @@
{
"af": [
"one",
"other"
],
"ak": [
"one",
"other"
],
"am": [
"one",
"other"
],
"an": [
"one",
"other"
],
"ar": [
"zero",
"one",
"two",
"few",
"many",
"other"
],
"ars": [
"zero",
"one",
"two",
"few",
"many",
"other"
],
"as": [
"one",
"other"
],
"asa": [
"one",
"other"
],
"ast": [
"one",
"other"
],
"az": [
"one",
"other"
],
"bal": [
"one",
"other"
],
"be": [
"one",
"few",
"many",
"other"
],
"bem": [
"one",
"other"
],
"bez": [
"one",
"other"
],
"bg": [
"one",
"other"
],
"bho": [
"one",
"other"
],
"bm": [
"other"
],
"bn": [
"one",
"other"
],
"bo": [
"other"
],
"br": [
"one",
"two",
"few",
"many",
"other"
],
"brx": [
"one",
"other"
],
"bs": [
"one",
"few",
"other"
],
"ca": [
"one",
"other"
],
"ce": [
"one",
"other"
],
"ceb": [
"one",
"other"
],
"cgg": [
"one",
"other"
],
"chr": [
"one",
"other"
],
"ckb": [
"one",
"other"
],
"cs": [
"one",
"few",
"many",
"other"
],
"cy": [
"zero",
"one",
"two",
"few",
"many",
"other"
],
"da": [
"one",
"other"
],
"de": [
"one",
"other"
],
"doi": [
"one",
"other"
],
"dsb": [
"one",
"two",
"few",
"other"
],
"dv": [
"one",
"other"
],
"dz": [
"other"
],
"ee": [
"one",
"other"
],
"el": [
"one",
"other"
],
"en": [
"one",
"other"
],
"eo": [
"one",
"other"
],
"es": [
"one",
"many",
"other"
],
"et": [
"one",
"other"
],
"eu": [
"one",
"other"
],
"fa": [
"one",
"other"
],
"ff": [
"one",
"other"
],
"fi": [
"one",
"other"
],
"fil": [
"one",
"other"
],
"fo": [
"one",
"other"
],
"fr": [
"one",
"many",
"other"
],
"fur": [
"one",
"other"
],
"fy": [
"one",
"other"
],
"ga": [
"one",
"two",
"few",
"many",
"other"
],
"gd": [
"one",
"two",
"few",
"other"
],
"gl": [
"one",
"other"
],
"gsw": [
"one",
"other"
],
"gu": [
"one",
"other"
],
"guw": [
"one",
"other"
],
"gv": [
"one",
"two",
"few",
"many",
"other"
],
"ha": [
"one",
"other"
],
"haw": [
"one",
"other"
],
"he": [
"one",
"two",
"many",
"other"
],
"hi": [
"one",
"other"
],
"hnj": [
"other"
],
"hr": [
"one",
"few",
"other"
],
"hsb": [
"one",
"two",
"few",
"other"
],
"hu": [
"one",
"other"
],
"hy": [
"one",
"other"
],
"ia": [
"one",
"other"
],
"id": [
"other"
],
"ig": [
"other"
],
"ii": [
"other"
],
"io": [
"one",
"other"
],
"is": [
"one",
"other"
],
"it": [
"one",
"many",
"other"
],
"iu": [
"one",
"two",
"other"
],
"ja": [
"other"
],
"jbo": [
"other"
],
"jgo": [
"one",
"other"
],
"jmc": [
"one",
"other"
],
"jv": [
"other"
],
"jw": [
"other"
],
"ka": [
"one",
"other"
],
"kab": [
"one",
"other"
],
"kaj": [
"one",
"other"
],
"kcg": [
"one",
"other"
],
"kde": [
"other"
],
"kea": [
"other"
],
"kk": [
"one",
"other"
],
"kkj": [
"one",
"other"
],
"kl": [
"one",
"other"
],
"km": [
"other"
],
"kn": [
"one",
"other"
],
"ko": [
"other"
],
"ks": [
"one",
"other"
],
"ksb": [
"one",
"other"
],
"ksh": [
"zero",
"one",
"other"
],
"ku": [
"one",
"other"
],
"kw": [
"zero",
"one",
"two",
"few",
"many",
"other"
],
"ky": [
"one",
"other"
],
"lag": [
"zero",
"one",
"other"
],
"lb": [
"one",
"other"
],
"lg": [
"one",
"other"
],
"lij": [
"one",
"other"
],
"lkt": [
"other"
],
"ln": [
"one",
"other"
],
"lo": [
"other"
],
"lt": [
"one",
"few",
"many",
"other"
],
"lv": [
"zero",
"one",
"other"
],
"mas": [
"one",
"other"
],
"mg": [
"one",
"other"
],
"mgo": [
"one",
"other"
],
"mk": [
"one",
"other"
],
"ml": [
"one",
"other"
],
"mn": [
"one",
"other"
],
"mo": [
"one",
"few",
"other"
],
"mr": [
"one",
"other"
],
"ms": [
"other"
],
"mt": [
"one",
"few",
"many",
"other"
],
"my": [
"other"
],
"nah": [
"one",
"other"
],
"naq": [
"one",
"two",
"other"
],
"nb": [
"one",
"other"
],
"nd": [
"one",
"other"
],
"ne": [
"one",
"other"
],
"nl": [
"one",
"other"
],
"nn": [
"one",
"other"
],
"nnh": [
"one",
"other"
],
"no": [
"one",
"other"
],
"nqo": [
"other"
],
"nr": [
"one",
"other"
],
"nso": [
"one",
"other"
],
"ny": [
"one",
"other"
],
"nyn": [
"one",
"other"
],
"om": [
"one",
"other"
],
"or": [
"one",
"other"
],
"os": [
"one",
"other"
],
"osa": [
"other"
],
"pa": [
"one",
"other"
],
"pap": [
"one",
"other"
],
"pcm": [
"one",
"other"
],
"pl": [
"one",
"few",
"many",
"other"
],
"prg": [
"zero",
"one",
"other"
],
"ps": [
"one",
"other"
],
"pt": [
"one",
"many",
"other"
],
"pt-PT": [
"one",
"many",
"other"
],
"rm": [
"one",
"other"
],
"ro": [
"one",
"few",
"other"
],
"rof": [
"one",
"other"
],
"ru": [
"one",
"few",
"many",
"other"
],
"rwk": [
"one",
"other"
],
"sah": [
"other"
],
"saq": [
"one",
"other"
],
"sat": [
"one",
"two",
"other"
],
"sc": [
"one",
"other"
],
"scn": [
"one",
"other"
],
"sd": [
"one",
"other"
],
"sdh": [
"one",
"other"
],
"se": [
"one",
"two",
"other"
],
"seh": [
"one",
"other"
],
"ses": [
"other"
],
"sg": [
"other"
],
"sh": [
"one",
"few",
"other"
],
"shi": [
"one",
"few",
"other"
],
"si": [
"one",
"other"
],
"sk": [
"one",
"few",
"many",
"other"
],
"sl": [
"one",
"two",
"few",
"other"
],
"sma": [
"one",
"two",
"other"
],
"smi": [
"one",
"two",
"other"
],
"smj": [
"one",
"two",
"other"
],
"smn": [
"one",
"two",
"other"
],
"sms": [
"one",
"two",
"other"
],
"sn": [
"one",
"other"
],
"so": [
"one",
"other"
],
"sq": [
"one",
"other"
],
"sr": [
"one",
"few",
"other"
],
"ss": [
"one",
"other"
],
"ssy": [
"one",
"other"
],
"st": [
"one",
"other"
],
"su": [
"other"
],
"sv": [
"one",
"other"
],
"sw": [
"one",
"other"
],
"syr": [
"one",
"other"
],
"ta": [
"one",
"other"
],
"te": [
"one",
"other"
],
"teo": [
"one",
"other"
],
"th": [
"other"
],
"ti": [
"one",
"other"
],
"tig": [
"one",
"other"
],
"tk": [
"one",
"other"
],
"tl": [
"one",
"other"
],
"tn": [
"one",
"other"
],
"to": [
"other"
],
"tpi": [
"other"
],
"tr": [
"one",
"other"
],
"ts": [
"one",
"other"
],
"tzm": [
"one",
"other"
],
"ug": [
"one",
"other"
],
"uk": [
"one",
"few",
"many",
"other"
],
"und": [
"other"
],
"ur": [
"one",
"other"
],
"uz": [
"one",
"other"
],
"ve": [
"one",
"other"
],
"vi": [
"other"
],
"vo": [
"one",
"other"
],
"vun": [
"one",
"other"
],
"wa": [
"one",
"other"
],
"wae": [
"one",
"other"
],
"wo": [
"other"
],
"xh": [
"one",
"other"
],
"xog": [
"one",
"other"
],
"yi": [
"one",
"other"
],
"yo": [
"other"
],
"yue": [
"other"
],
"zh": [
"other"
],
"zu": [
"one",
"other"
]
}

View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2017",
"lib": [
"DOM",
"ES5",
"ES6",
"ES7",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ES2021",
"ESNext"
],
"rootDir": "./assets",
"declaration": true,
"declarationDir": "../@types/runtime",
"emitDeclarationOnly": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": ["../@types"],
"types": [
"intl",
"@cocos/creator-types/editor",
"@cocos/creator-types/engine"
],
},
"exclude": [
"../@types/**/*"
],
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
'use strict';
/**
* 需要编译的 ts 文件夹
*/
exports.js = function() {
return ['./'];
}
/**
* 需要编译的 less 文件夹
*/
exports.css = function() {
return [];
}

View File

@@ -0,0 +1,13 @@
'use strict';
const { join } = require('path');
exports['build-typescript-develop'] = async function() {
return [
__dirname,
];
};
exports['build-less-develop'] = function() {
return [];
};

View File

@@ -0,0 +1,58 @@
# plugin-import-2.x
[中文](https://github.com/cocos-creator/plugin-import-2.x/blob/main/README.zh-cn.md)
[Cocos Creator 3.0 Upgrade Guide](https://github.com/cocos-creator/creator-docs/blob/v3.0/en/release-notes/upgrade-guide-v3.0.md)
This plugin is used to reduce the workload of developers upgrading v2.x projects to v3.0.0.
As the editor does not currently support hot updates for plugins, if developers encounter problems with the plugin, use the tutorial below to update the plugin so that they can quickly fix the problem without having to wait for the editor version to be updated.
## Update Notes
- Optimize the interface to add version display and prompt after import
## How to update the plugin
### Editor Version >= 3.7.0
1. Open Extension Manager from the main menu
![img.png](https://user-images.githubusercontent.com/7564028/232646751-dbb30224-00f2-4b4f-9a92-d9c150fe81d2.png)
2. Find the **plugin-import-2x** plugin and click Install or Update to get the latest version
![img_1.png](https://user-images.githubusercontent.com/7564028/232646762-a07b971d-0e38-4808-8b4d-d884baef3591.png)
### Editor Version <= 3.6.x
1. Store in the relevant designated location, as follows
- To apply globally (all projects), just store the plugins folder under **User/.CocosCreator/extensions**
- To apply to a single project, simply store the folder in the **extensions** folder at the same level as the **assets** file
> **Note**: If you do not have an **extensions** folder, you will need to create one yourself
2. Enable Extension
1.Open Extension Manager via the main menu
![img](https://user-images.githubusercontent.com/7564028/114006756-49c20a80-9893-11eb-8744-30215330a10b.png)
2.Click on the Refresh button
![img](https://user-images.githubusercontent.com/7564028/114006766-4c246480-9893-11eb-9f46-b0fe03c2c09b.png)
3.Enable extension
![img](https://user-images.githubusercontent.com/7564028/114006763-4b8bce00-9893-11eb-88ba-e39e3d00a22a.png)
> **NOTE**: If 2 menus appear, restart the editor. (This is a known issue and will be fixed later)
## How to give feedback
1. [New **New issue**](https://github.com/cocos-creator/plugin-import-2.x/issues/new)
2. [Forum](https://forum.cocos.org/c/Creator)
If an existing project needs to be upgraded under special circumstances, and technical or workload difficulties are encountered, please contact [liyan@cocos.com](mailto:liyan@cocos.com) for assistance!

View File

@@ -0,0 +1,59 @@
# plugin-import-2.x
[English](https://github.com/cocos-creator/plugin-import-2.x/blob/main/README.md)
[Cocos Creator 3.0 升级指南](https://github.com/cocos-creator/creator-docs/blob/v3.0/zh/release-notes/upgrade-guide-v3.0.md)
该插件用于减少开发者升级 v2.x 项目到 v3.0.0 的工作量。
由于目前编辑器无法支持插件热更新,所以如果开发者在使用该插件时遇到问题,通过下方教程去更新插件,从而能够快速解决问题,无需等待编辑器版本更新。
## 更新说明
- 优化界面添加版本显示以及导入后的提示
## 如何更新插件
### 编辑器版本 >= 3.7.0
1. 通过主菜单打开扩展管理器
![img.png](https://user-images.githubusercontent.com/7564028/232646751-dbb30224-00f2-4b4f-9a92-d9c150fe81d2.png)
2. 找到 **plugin-import-2x** 插件点击安装或者更新,获取最新版本
![img_1.png](https://user-images.githubusercontent.com/7564028/232646762-a07b971d-0e38-4808-8b4d-d884baef3591.png)
### 编辑器版本 <= 3.6.x
1. 存放到相关指定位置,如下
- 应用于全局(所有项目)下,只需要将插件文件夹存放到 **用户/.CocosCreator/extensions**
- 应用于单个项目下,只需要将文件夹存放到与 **assets** 文件同级的 **extensions** 文件夹下
> **注意**: 如果没有 **extensions** 文件夹,需要自行创建一个
2. 启用插件
1.通过主菜单打开插件管理器
![img](https://user-images.githubusercontent.com/7564028/114006756-49c20a80-9893-11eb-8744-30215330a10b.png)
2.点击刷新按钮
![img](https://user-images.githubusercontent.com/7564028/114006766-4c246480-9893-11eb-9f46-b0fe03c2c09b.png)
3.启用插件
![img](https://user-images.githubusercontent.com/7564028/114006763-4b8bce00-9893-11eb-88ba-e39e3d00a22a.png)
> **注意**: 如果出现 2 个菜单的话,重启编辑器即可。(该问题是已知问题,后续会修复)
## 如何反馈问题
1. [新建 **New issue**](https://github.com/cocos-creator/plugin-import-2.x/issues/new)
2. [论坛](https://forum.cocos.org/c/Creator)
如果现有项目因为特殊原因需要升级,并且遇到了技术上或者工作量上的困难,也可以联系 [liyan@cocos.com](mailto:liyan@cocos.com) 获取我们的人工协助!

View File

@@ -0,0 +1,36 @@
'use strict';
declare const Editor: any;
/**
* 插件定义的方法
* Methods defined by extension
* 可以在 package.json 里的 contributions 里定义 messages 触发这里的方法
* And of course, messages can be defined in the contributions section in package.JSON to trigger the method here
*/
exports.methods = {
async importCreatorProject() {
const result = await Editor.Dialog.select({
title: Editor.I18n.t('plugin-import-2x.select_dialog.title'),
path: await Editor.Profile.getConfig('plugin-import-2x', 'import-path') || Editor.Project.path,
type: 'directory',
});
if (!result.filePaths || !result.filePaths[0]) {
return;
}
Editor.Profile.setConfig('plugin-import-2x', 'import-path', result.filePaths[0]);
Editor.Panel.open('plugin-import-2x.creator');
},
};
/**
* 启动的时候执行的初始化方法
* Initialization method performed at startup
*/
exports.load = function() {};
/**
* 插件被关闭的时候执行的卸载方法
* Uninstall method performed when the extension is closed
*/
exports.unload = function() {};

View File

@@ -0,0 +1,474 @@
/**
* 用于导入 2d 项目到 3d 项目
*/
'use strict';
// @ts-ignore
import { v4 } from 'node-uuid';
import { nameToId } from './utlis';
import { relative, join, dirname, parse, extname, ParsedPath } from 'path';
// @ts-ignore
import { existsSync, ensureDirSync, copyFileSync, readJSONSync, writeJSONSync, readFileSync, writeFileSync } from 'fs-extra';
import {
migratePlatformSettings,
saveUuid,
getDefaultAssets2D,
getNewUuid,
import2DChunks,
importSubAssets, importProjectAssets,
} from './utlis';
import { UUID_2D_TO_3D, UUID_SKIP_EFFECT, UUID_UI_2D_TO_3D } from "./diff";
import { getConverter } from "../convertor";
export interface MessageInfo {
type: string; // 类型表示需要处理什么资源
json: any; // 实际资源的源数据
}
export abstract class ImporterBase {
public type: string = '';
// 导入到 3d 工程所在磁盘的路径
protected destFsPath: string = '';
protected destMetaFsPath: string = '';
// 2d 源文件所在磁盘的路径
protected sourceFsPath: string = '';
protected pathInfo: ParsedPath | null = null;
protected _2dMeta: any = null;
protected _3dMeta: any = null;
// 2d 源文件转成 3d 源文件,如果不为 null 说明需要保存
// 例如 animation、prefab、scene 之类的源文件
protected _2dTo3dSource: any = null;
// 检查 uuid 是否冲突,如果有就存储起来,后续会用到
async checkUuid(meta: any) {
const assetFsPath = await Editor.Message.request('asset-db', 'query-path', meta.uuid);
if (assetFsPath && assetFsPath !== this.destFsPath) {
const newUuid = v4();
saveUuid(meta.uuid, newUuid);
return newUuid;
}
// 存放 sprite frame uuid 对应的 texture uuid
if (meta.type === 'sprite' && meta.subMetas) {
for (const key in meta.subMetas) {
const subMeta = meta.subMetas[key];
saveUuid(subMeta.uuid, subMeta.rawTextureUuid);
}
}
return meta.uuid;
}
get3DUuid() {
try {
const meta = readJSONSync(this.destMetaFsPath);
return meta.uuid;
}
catch (e) {
return v4();
}
}
public reset() {
this.destFsPath = '';
this.sourceFsPath = '';
this.pathInfo = null;
this._2dMeta = null;
this._3dMeta = null;
this._2dTo3dSource = null;
}
public static getPathInfo(projectRoot: string, sourceFsPath: string) {
let relativePath = relative(projectRoot, sourceFsPath);
if (!relativePath.startsWith('assets')) {
relativePath = join('assets', relativePath);
}
let to = join(Editor.Project.path, relativePath);
// 改后缀名 .fire to .scene;
if (to.endsWith('.fire')) {
to = to.replace(/.fire+$/g, '.scene');
} else if (to.endsWith('.js')) {
const meta = readJSONSync(sourceFsPath + '.meta');
if (!meta.isPlugin) {
to = to.replace(/.js+$/g, '.ts');
}
}
return {
to: to,
toMeta: to + '.meta',
from: sourceFsPath,
fromMeta: sourceFsPath + '.meta',
pathInfo: parse(sourceFsPath),
};
}
public static isNew(projectRoot: string, sourceFsPath: string) {
try {
if (sourceFsPath.endsWith('assets')) {
return false;
}
const info = ImporterBase.getPathInfo(projectRoot, sourceFsPath);
if (existsSync(info.to) && existsSync(info.toMeta)) {
const _3DMeta = readJSONSync(info.toMeta);
if (_3DMeta.importer === 'directory') {
return false;
}
const _2DMeta = readJSONSync(info.fromMeta);
return _2DMeta.uuid !== _3DMeta.uuid;
}
return true;
}
catch (e) {
return true;
}
}
/*
* 导入前
* 参数一:项目的路径
* 参数二:项目的资源路径
*/
public async beforeImport(projectRoot: string, sourceFsPath: string): Promise<boolean> {
// 重置
this.reset();
//
this.sourceFsPath = sourceFsPath;
const info = ImporterBase.getPathInfo(projectRoot, sourceFsPath);
this.destFsPath = info.to;
this.destMetaFsPath = info.toMeta;
this.pathInfo = info.pathInfo;
// update asset meta
this._2dMeta = this.read2dMeta(sourceFsPath);
// 检查 uuid 是否冲突
const newUuid = this._2dMeta ? await this.checkUuid(this._2dMeta) : this.get3DUuid();
this._3dMeta = this.createNewMeta(newUuid);
this._3dMeta.uuid = newUuid;
this._2dMeta && importProjectAssets.set(this._2dMeta.uuid, {
type: extname(sourceFsPath),
basePath: sourceFsPath,
outPath: this.destMetaFsPath,
outUuid: this._2dMeta.uuid,
meta: this._2dMeta,
});
return true;
}
public needImport() {
let doImport = true;
if (existsSync(this.destFsPath) && existsSync(this.destMetaFsPath)) {
const meta = readJSONSync(this.destMetaFsPath);
doImport = this._3dMeta.uuid !== meta.uuid;
}
if (doImport) {
// console.log(Editor.I18n.t('plugin-import-2x.import_log', {
// path: this.sourceFsPath,
// }));
}
return doImport;
}
/*
* 导入并且进行转换
*/
public async import(main?: any): Promise<boolean> {
return true;
}
/*
* 转换后进行报错跟拷贝源文件的处理
*/
public async afterImport() {
this.copySync(this.sourceFsPath);
if (this._2dTo3dSource) {
try {
if (this.destFsPath.endsWith('.ts') ||
this.destFsPath.endsWith('.js') ||
this.destFsPath.endsWith('.plist') ||
this.destFsPath.endsWith('.effect')) {
writeFileSync(this.destFsPath, this._2dTo3dSource, { encoding: 'utf8' });
}
else {
writeJSONSync(this.destFsPath, this._2dTo3dSource, { spaces: 2 });
}
}
catch (e) { console.error(e); }
}
// console.log('保存:' + this.destFsPath);
this.saveMeta();
}
/*
* 创建新的 meta 对象
*/
public createNewMeta(uuid?: string) {
return {
uuid: uuid || '',
imported: false,
importer: '*',
files: [],
subMetas: {},
userData: {},
ver: '0.0.1',
};
}
/*
* 读取 meta
*/
public read2dMeta(sourceFsPath: string) {
try {
if (!sourceFsPath.endsWith('.meta')) {
sourceFsPath += '.meta';
}
if (!existsSync(sourceFsPath)) {
return null;
}
return readJSONSync(sourceFsPath);
}
catch (e) {
console.error(e);
return null;
}
}
/*
* 拷贝资源 + meta
*/
public copySync(from: string, to?: string) {
try {
if (!existsSync(from)) {
return 0;
}
if (to) {
ensureDirSync(dirname(to));
copyFileSync(from, to);
} else {
ensureDirSync(dirname(this.destFsPath));
copyFileSync(from, this.destFsPath);
}
}
catch (e) {
console.error(e);
}
}
/*
* 保存 meta
*/
public async saveMeta() {
if (!this.destMetaFsPath.endsWith('.meta')) {
this.destMetaFsPath += '.meta';
}
try {
writeJSONSync(this.destMetaFsPath, this._3dMeta, { spaces: 2 });
}
catch (e) {
console.error(e);
}
}
/*
* 保存
*/
public writeFileSync(to: string, data: any) {
try {
writeFileSync(to, data, { encoding: 'utf8' });
}
catch (e) {
console.error(e);
}
}
/*
* 加载源文件类型为 JSON
*/
public readJSONSync(sourceFsPath?: string) {
try {
return readJSONSync(sourceFsPath || this.sourceFsPath);
}
catch (e) {
console.error(e);
return null;
}
}
/*
* 加载源文件
*/
public readFileSync(sourceFsPath?: string) {
try {
return readFileSync(sourceFsPath || this.sourceFsPath, 'utf8');
}
catch (e) {
console.error(e);
return null;
}
}
/*
* 导入缓存纹理设置
*/
public async migratePlatformSettings(platformSettings: any) {
return await migratePlatformSettings(platformSettings);
}
static async getUuid(uuid: string, type?: string) {
if (UUID_2D_TO_3D.has(uuid)) {
return UUID_2D_TO_3D.get(uuid);
}
if (UUID_UI_2D_TO_3D.has(uuid)) {
return UUID_UI_2D_TO_3D.get(uuid);
}
if (UUID_SKIP_EFFECT.has(uuid)) {
console.warn(Editor.I18n.t('plugin-import-2x.effect_warn_tips', {
name: UUID_SKIP_EFFECT.get(uuid) as string,
}));
}
uuid = await ImporterBase.ensureDefaultAssets2DFor3D(uuid);
uuid = getNewUuid(uuid);
if (type && !uuid.includes('@')) {
const id = `@${ImporterBase.getNameByID(type)}`;
if (!uuid.endsWith(id)) {
uuid += id;
}
}
return uuid;
}
static getNewUuid(uuid: string) {
return getNewUuid(uuid);
}
/*
* 通过名字获取 id
*/
static getNameByID(name: string) {
return nameToId(name);
}
/*
* 创建默认资源
*/
static getDefaultAssets2D(uuid: string) {
return getDefaultAssets2D(uuid);
}
/*
* 创建 2d 默认资源
*/
static async ensureDefaultAssets2DFor3D(uuid: string) {
const subAssets = importSubAssets.get(uuid);
if (subAssets) {
uuid = subAssets.baseUuid;
}
else {
const projectAssets = importProjectAssets.get(uuid);
if (projectAssets) {
uuid = projectAssets.outUuid;
}
}
const info = getDefaultAssets2D(uuid);
if (info && info.path) {
// 如果是内置资源与 3d 的一致就直接用 3D 的
if (UUID_UI_2D_TO_3D.has(info.baseUuid)) {
return UUID_UI_2D_TO_3D.get(info.baseUuid);
}
if (UUID_2D_TO_3D.has(info.baseUuid)) {
return UUID_2D_TO_3D.get(info.baseUuid);
}
const defaultAssetsRootPath = join(__dirname, '../../static');
let relativePath = relative(defaultAssetsRootPath, info.path);
if (!relativePath.startsWith('assets')) {
relativePath = join('assets', relativePath);
}
const destFsPath = join(Editor.Project.path, relativePath);
try {
if (!existsSync(destFsPath)) {
if (destFsPath.endsWith('.mtl') || destFsPath.endsWith('.effect')) {
await import2DChunks(false);
}
const converter = getConverter(extname(info.path));
if (converter) {
await converter.beforeImport(defaultAssetsRootPath, info.path);
const isDone = await converter.import();
if (isDone) {
await converter.afterImport();
}
}
const readmePath = join(Editor.Project.path, 'asset', 'migrate-resources', 'README.md');
if (!existsSync(readmePath)) {
ensureDirSync(dirname(readmePath));
writeFileSync(readmePath, readFileSync(join(defaultAssetsRootPath, 'migrate-resources', 'README.md'), {encoding: 'utf8'}));
}
}
}
catch (e) {
console.error(e);
}
if (subAssets) {
return subAssets.uuid;
}
return info.baseUuid;
}
if (subAssets) {
return subAssets.uuid;
}
return uuid;
}
/*
* 通过 engine 进行序列化与反序列化
*/
async queryCCClass(engine: any, message: any) {
engine.contentWindow.postMessage(message, '*');
return new Promise((resolve, reject) => {
function onMessageCb(event: any) {
window.removeEventListener("message", onMessageCb, false);
resolve(event.data);
}
window.addEventListener("message", onMessageCb, false);
});
}
replaceScript(name: string) {
try {
const defaultAssetsRootPath = join(__dirname, '../../static/migrate-resources/default-assets-2d/scripts');
const fromFsPath = join(defaultAssetsRootPath, name);
const destFsPath = join(Editor.Project.path, 'assets', 'default-assets-2d', 'scripts', name);
if (!existsSync(destFsPath)) {
ensureDirSync(dirname(destFsPath));
copyFileSync(fromFsPath, destFsPath);
}
const fromMetaPath = fromFsPath + '.meta';
const destMetaFsPath = destFsPath + '.meta';
if (!existsSync(destMetaFsPath)) {
ensureDirSync(dirname(destMetaFsPath));
copyFileSync(fromMetaPath, destMetaFsPath);
}
const meta = readJSONSync(fromMetaPath);
// @ts-ignore
const EditorExtends: any = require('@base/electron-module').require('EditorExtends');
const UuidUtils = EditorExtends.UuidUtils;
return UuidUtils.compressUuid(meta.uuid, false);
}
catch (e) {
console.error(e);
return '';
}
}
//
ensureDefaultSprite2DFor3D(json3D: any) {
for (const key in json3D) {
const item = json3D[key];
if (item.__type__ === 'cc.StudioComponent') {
item.__type__ = this.replaceScript('studio-component.ts');
}
else if (item.__type__ === 'cc.StudioWidget') {
item.__type__ = this.replaceScript('studio-widget.ts');
}
}
return json3D;
}
}

View File

@@ -0,0 +1,127 @@
export const UUID_2D_TO_3D: Map<string, string> = new Map<string, string>();
export const UUID_UI_2D_TO_3D: Map<string, string> = new Map<string, string>();
export const UUID_SKIP_EFFECT: Map<string, string> = new Map<string, string>();
export function initDiff() {
// builtin-standard.effect -> builtin-phong.effect
UUID_2D_TO_3D.set('abc2cb62-7852-4525-a90d-d474487b88f2', '1baf0fc9-befa-459c-8bdd-af1a450a0319');
// builtin-2d-graphics -> builtin-graphics
UUID_2D_TO_3D.set('30682f87-9f0d-4f17-8a44-72863791461b', '1c02ae6f-4492-4915-b8f8-7492a3b1e4cd');
// builtin-2d-spine -> builtin-spine
UUID_2D_TO_3D.set('0e93aeaa-0b53-4e40-b8e0-6268b4e07bd7', '7383da24-dfde-48e8-82a7-a6e8a56f285c');
// builtin-2d-sprite -> builtin-sprite
UUID_2D_TO_3D.set('2874f8dd-416c-4440-81b7-555975426e93', '60f7195c-ec2a-45eb-ba94-8955f60e81d0');
// builtin-3d-particle -> builtin-particle
UUID_2D_TO_3D.set('829a282c-b049-4019-bd38-5ace8d8a6417', 'd1346436-ac96-4271-b863-1f4fdead95b0');
// builtin-3d-trail -> builtin-particle-trail
UUID_2D_TO_3D.set('2a7c0036-e0b3-4fe1-8998-89a54b8a2bec', '17debcc3-0a6b-4b8a-b00b-dc58b885581e');
// builtin-clear-stencil
UUID_2D_TO_3D.set('cf7e0bb8-a81c-44a9-ad79-d28d43991032', '810e96e4-e456-4468-9b59-f4e8f39732c0');
// builtin-unlit
UUID_2D_TO_3D.set('6d91e591-4ce0-465c-809f-610ec95019c6', 'a3cd009f-0ab0-420d-9278-b9fdab939bbc');
// builtin-toon
UUID_2D_TO_3D.set('e2f00085-c597-422d-9759-52c360279106', 'a7612b54-35e3-4238-a1a9-4a7b54635839');
// builtin-2d-sprite -> ui-sprite-material
UUID_2D_TO_3D.set('eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432', 'fda095cb-831d-4601-ad94-846013963de8');
// builtin-2d-label -> ui-sprite-material
UUID_2D_TO_3D.set('e02d87d4-e599-4d16-8001-e14891ac6506', 'fda095cb-831d-4601-ad94-846013963de8');
// builtin-2d-gray-sprite -> ui-sprite-material
UUID_2D_TO_3D.set('3a7bb79f-32fd-422e-ada2-96f518fed422', 'fda095cb-831d-4601-ad94-846013963de8');
// primitives
UUID_2D_TO_3D.set('954fec8b-cd16-4bb9-a3b7-7719660e7558', '1263d74c-8167-4928-91a6-4e2672411f47');
// box.mesh
UUID_2D_TO_3D.set('046f172c-1574-488b-bbb8-6415a9adb96d', '1263d74c-8167-4928-91a6-4e2672411f47@a804a');
// capsule.mesh
UUID_2D_TO_3D.set('83f5eff8-3385-4f95-9b76-8da0aa1d96cd', '1263d74c-8167-4928-91a6-4e2672411f47@801ec');
// cone.mesh
UUID_2D_TO_3D.set('7a17de6e-227a-46b1-8009-e7157d4d3acf', '1263d74c-8167-4928-91a6-4e2672411f47@38fd2');
// cylinder.mesh
UUID_2D_TO_3D.set('b430cea3-6ab3-4106-b073-26c698918edd', '1263d74c-8167-4928-91a6-4e2672411f47@8abdc');
// DefaultMaterial
UUID_2D_TO_3D.set('a5849239-3ad3-41d1-8ab4-ae9fea11f97f', '1263d74c-8167-4928-91a6-4e2672411f47@ea6e2');
// plane.mesh
UUID_2D_TO_3D.set('a1ef2fc9-9c57-418a-8f69-6bed9a7a0e7f', '1263d74c-8167-4928-91a6-4e2672411f47@2e76e');
// primitives.prefab
UUID_2D_TO_3D.set('ab2fdde9-10c2-44e4-bfe1-fcfcc1a86aa9', '1263d74c-8167-4928-91a6-4e2672411f47@aae0f');
// quad.mesh
UUID_2D_TO_3D.set('e93d3fa9-8c21-4375-8a21-14ba84066c77', '1263d74c-8167-4928-91a6-4e2672411f47@fc873');
// sphere.mesh
UUID_2D_TO_3D.set('3bbdb0f6-c5f6-45de-9f33-8b5cbafb4d6d', '1263d74c-8167-4928-91a6-4e2672411f47@17020');
// torus.mesh
UUID_2D_TO_3D.set('14c74869-bdb4-4f57-86d8-a7875de2be30', '1263d74c-8167-4928-91a6-4e2672411f47@40ece');
// box -> cube
UUID_2D_TO_3D.set('a87cc147-01b2-43f8-8e42-a7ca90b0c757', '30da77a1-f02d-4ede-aa56-403452ee7fde');
// capsule
UUID_2D_TO_3D.set('fe1417b6-fe6b-46a4-ae7c-9fd331f33a2a', '73ce1f7f-d1f4-4942-ad93-66ca3b3041ab');
// cone
UUID_2D_TO_3D.set('b5fc2cf2-7942-483d-be1f-bbeadc4714ad', '6350d660-e888-4acf-a552-f3b719ae9110');
// Cylinder
UUID_2D_TO_3D.set('1c5e4038-953a-44c2-b620-0bbfc6170477', 'ab3e16f9-671e-48a7-90b7-d0884d9cbb85');
// Plane
UUID_2D_TO_3D.set('3f376125-a699-40ca-ad05-04d662eaa1f2', '40563723-f8fc-4216-99ea-a81636435c10');
// Quad
UUID_2D_TO_3D.set('6c9ef10d-b479-420b-bfe6-39cdda6a8ae0', '34a07346-9f62-4a84-90ae-cb83f7a426c1');
// Sphere
UUID_2D_TO_3D.set('2d9a4b85-b0ab-4c46-84c5-18f393ab2058', '655c9519-1a37-472b-bae6-29fefac0b550');
// Torus
UUID_2D_TO_3D.set('de510076-056b-484f-b94c-83bef217d0e1', 'd47f5d5e-c931-4ff4-987b-cc818a728b82');
//
UUID_SKIP_EFFECT.set('abc2cb62-7852-4525-a90d-d474487b88f2', 'builtin-phong.effect');
// 内置 UI 替换
// default-particle.png
UUID_UI_2D_TO_3D.set('600301aa-3357-4a10-b086-84f011fa32ba', 'b5b27ab1-e740-4398-b407-848fc2b2c897');
// default_btn_disabled.png
UUID_UI_2D_TO_3D.set('71561142-4c83-4933-afca-cb7a17f67053', '951249e0-9f16-456d-8b85-a6ca954da16b');
// default_btn_normal.png
UUID_UI_2D_TO_3D.set('e851e89b-faa2-4484-bea6-5c01dd9f06e2', '20835ba4-6145-4fbc-a58a-051ce700aa3e');
// default_btn_pressed.png
UUID_UI_2D_TO_3D.set('b43ff3c2-02bb-4874-81f7-f2dea6970f18', '544e49d6-3f05-4fa8-9a9e-091f98fc2ce8');
// default_editbox_bg.png
UUID_UI_2D_TO_3D.set('edd215b9-2796-4a05-aaf5-81f96c9281ce', 'bd1bcaba-bd7d-4a71-b143-997c882383e4');
// default_panel.png
UUID_UI_2D_TO_3D.set('d81ec8ad-247c-4e62-aa3c-d35c4193c7af', 'b730527c-3233-41c2-aaf7-7cdab58f9749');
// default_progressbar.png
UUID_UI_2D_TO_3D.set('cfef78f1-c8df-49b7-8ed0-4c953ace2621', '24a704da-2867-446d-8d1a-5e920c75e09d');
// default_progressbar_bg.png
UUID_UI_2D_TO_3D.set('99170b0b-d210-46f1-b213-7d9e3f23098a', '9fd900dd-221b-4f89-8f2c-fba34243c835');
// default_radio_button_off.png
UUID_UI_2D_TO_3D.set('567dcd80-8bf4-4535-8a5a-313f1caf078a', 'f12a23c4-b924-4322-a260-3d982428f1e8');
// default_radio_button_on.png
UUID_UI_2D_TO_3D.set('9d60001f-b5f4-4726-a629-2659e3ded0b8', '45828f25-b50d-4c52-a591-e19491a62b8c');
// default_scrollbar.png
UUID_UI_2D_TO_3D.set('0291c134-b3da-4098-b7b5-e397edbe947f', '0da256a2-21f6-481b-90b6-d3643a09179b');
// default_scrollbar_bg.png
UUID_UI_2D_TO_3D.set('4bab67cb-18e6-4099-b840-355f0473f890', '28765e2f-040a-4c65-8e8c-f9d0bb79d863');
// default_scrollbar_vertical.png
UUID_UI_2D_TO_3D.set('d6d3ca85-4681-47c1-b5dd-d036a9d39ea2', 'afc47931-f066-46b0-90be-9fe61f213428');
// default_scrollbar_vertical_bg.png
UUID_UI_2D_TO_3D.set('617323dd-11f4-4dd3-8eec-0caf6b3b45b9', 'ffb88a8f-af62-48f4-8f1d-3cb606443a43');
// default_sprite.png
UUID_UI_2D_TO_3D.set('6e056173-d285-473c-b206-40a7fff5386e', '57520716-48c8-4a19-8acf-41c9f8777fb0');
// default_sprite_splash.png
UUID_UI_2D_TO_3D.set('0275e94c-56a7-410f-bd1a-fc7483f7d14a', '7d8f9b89-4fd1-4c9f-a3ab-38ec7cded7ca');
// default_toggle_checkmark.png
UUID_UI_2D_TO_3D.set('73a0903d-d80e-4e3c-aa67-f999543c08f5', '158e7e52-3220-4cd7-9694-713e0e6e8278');
// default_toggle_disabled.png
UUID_UI_2D_TO_3D.set('c25b9d50-c8fc-4d27-beeb-6e7c1f2e5c0f', 'ca7e121b-293c-4763-829a-b7a5fa81f0d2');
// default_toggle_normal.png
UUID_UI_2D_TO_3D.set('d29077ba-1627-4a72-9579-7b56a235340c', '11bdc4b0-64a8-4eb7-a2a7-9fb9e233e977');
// default_toggle_pressed.png
UUID_UI_2D_TO_3D.set('b181c1e4-0a72-4a91-bfb0-ae6f36ca60bd', 'a04e994f-ee49-47b6-9d08-2f59e3773fcc');
// atom
UUID_UI_2D_TO_3D.set('b8223619-7e38-47c4-841f-9160c232495a', '86f25d5c-9de5-454f-a5f9-ee16603e6701');
UUID_UI_2D_TO_3D.set('b2687ac4-099e-403c-a192-ff477686f4f5', '86f25d5c-9de5-454f-a5f9-ee16603e6701');
// atom.png texture
UUID_UI_2D_TO_3D.set('8a96b965-2dc0-4e03-aa90-3b79cb93b5b4', '24c419ea-63a8-4ea1-a9d0-7fc469489bbc@6c48a');
UUID_UI_2D_TO_3D.set('d0a82d39-bede-46c4-b698-c81ff0dedfff', '24c419ea-63a8-4ea1-a9d0-7fc469489bbc@6c48a');
// atom.png sprite-frame
UUID_UI_2D_TO_3D.set('bb42ed8e-0867-4584-ad63-b6f84f83bba8', '24c419ea-63a8-4ea1-a9d0-7fc469489bbc@f9941');
UUID_UI_2D_TO_3D.set('472df5d3-35e7-4184-9e6c-7f41bee65ee3', '24c419ea-63a8-4ea1-a9d0-7fc469489bbc@f9941');
// 3d 粒子
UUID_UI_2D_TO_3D.set('432fa09c-cf03-4cff-a186-982604408a07', 'ea7478b0-408d-4052-b703-f0d2355e095f');
// video
UUID_UI_2D_TO_3D.set('2be36297-9abb-4fee-8049-9ed5e271da8a', '2be36297-9abb-4fee-8049-9ed5e271da8a');
}

View File

@@ -0,0 +1,810 @@
import { readWriteFileByLineWithProcess } from "./utlis";
function getType(val: string) {
if (!isNaN(Number(val))) {
return undefined;
}
if (val === 'false' || val === 'true') {
return undefined;
}
if (val === 'null' || val === 'undefined') {
return undefined;
}
// 数组
if (val.startsWith('[') && val.endsWith(']')) {
return [];
}
val = val.split('(')[0];
if (!val.includes('cc')) {
return undefined;
}
if (val.startsWith('cc.')) {
const array = val.split('.');
if (array.length > 3) {
return array[1];
}
}
return val;
}
function getInfo(line: string, skip: boolean = false) {
let values = line.split(':');
if (values.length <= 1) {
// 函数
values = line.split('(');
}
if (values.length <= 1) {
return {
key: line,
value: line,
};
}
let value = values[1].trim().split(',')[0];
if (!skip) {
value = value.replace(/'|"|,/g, '');
}
return {
key: values[0].trim(),
value: value,
};
}
function syncIndex(line: string, index: number) {
let result = line.match(/\{/g);
if (result && result.length > 0) {
index += result.length;
}
result = line.match(/\}/g);
if (result && result.length > 0) {
index -= result.length;
}
return index;
}
function createContent(name: string) {
return {
name: name,
extends: '',
mixins: '',
editors: {},
statics: {},
properties: {},
functions: {},
};
}
export async function parseJSCode(path: string, name: string) {
let otherIndex = 0;
let classCodeIndex = 0;
const classCodeMap: Map<number, any> = new Map();
const importCodeMap: Map<number, string> = new Map();
const otherCodeMap: Map<number, string> = new Map();
const endCodeMap: Map<number, string> = new Map();
const ccKeys: string[] = [];
let openClass: boolean = false;
let classIndex = 0;
let openName: boolean | undefined = undefined;
let openExtends: boolean | undefined = undefined;
let openMixins: boolean | undefined = undefined;
let openEditors: boolean | undefined = undefined;
let propTotalIndex = 0;
let subPropName = '';
let subPropIndex = 0;
let hasGet: boolean | undefined = undefined;
let getIndex = 0;
let hasSet: boolean | undefined = undefined;
let setIndex = 0;
let hasNotify: boolean | undefined = undefined;
let notifyIndex = 0;
let openSubProp: boolean | undefined = undefined;
let openProperties: boolean | undefined = undefined;
let staticIndex = 0;
let subStaticIndex = 0;
let subStaticName: string | undefined = undefined;
let openStatics: boolean | undefined = undefined;
let funcName = '';
let funcIndex = 0;
let openFunctions: boolean | undefined = undefined;
let content: any;
let isSkips = false;
let topNote: string = '';
await readWriteFileByLineWithProcess(path, (line: string) => {
try {
// 剔除空格
let noTrimLine = line;
line = line.trim();
if (line.startsWith('/*')) {
isSkips = true;
topNote += (line + '\n');
return;
}
if (isSkips) {
isSkips = !line.endsWith('*/');
topNote += (line + '\n');
return;
}
// 直接过滤注释文字
if (line.startsWith('/') || line.startsWith('*') || !line) {
return;
}
if (!openClass) {
if (line.includes('cc.Class(')) {
openClass = true;
classIndex = 1;
classCodeIndex = classCodeMap.size;
content = createContent(name);
if (!classCodeMap.has(classCodeIndex)) {
classCodeMap.set(classCodeIndex, content);
}
} else {
if (line.includes('require')) {
importCodeMap.set(importCodeMap.size, line);
} else {
const ccKeyArr = line.match(/(?<=cc.)(.*?)(?=[.|,|;|)|}|(])/);
const ccKey = ccKeyArr && ccKeyArr[0];
if (ccKey) {
ccKeys.push(ccKey);
}
if (classCodeMap.size === 0) {
otherIndex = syncIndex(line, otherIndex);
if (line.includes('cc.runtime')) {
otherIndex -= 1;
noTrimLine = '//' + noTrimLine;
}
if (ccKey) {
if (ccKey.includes('=') || ccKey.includes('function')) {
noTrimLine = noTrimLine.replace(`cc.${ccKey}`, `const ${ccKey}`);
}
else {
noTrimLine = noTrimLine.replace(`cc.${ccKey}`, ccKey);
}
const multiple = noTrimLine.match(new RegExp(ccKey, 'g'));
if (multiple && multiple.length > 1) {
noTrimLine = '//' + noTrimLine;
}
}
otherCodeMap.set(otherCodeMap.size, noTrimLine);
} else if (classCodeMap.size > 0) {
otherIndex = syncIndex(line, otherIndex);
if (otherIndex < 0) {
noTrimLine = '//' + line;
}
endCodeMap.set(endCodeMap.size, noTrimLine);
}
}
}
} else if (openClass) {
// --------------- 检测是否解析类完毕 ---------------
classIndex = syncIndex(line, classIndex);
if (classIndex === 0 &&
(line.endsWith('});') || line.endsWith('})') || line.endsWith(');') || line.endsWith(')') || line.endsWith(';'))) {
openClass = false;
classCodeMap.set(classCodeIndex, content);
return;
}
if (openProperties === undefined && openFunctions === undefined && openStatics === undefined) {
// --------------- 获取 name ---------------
if (openName === undefined && line.startsWith('name:')) {
openName = true;
}
if (openName) {
content.name = getInfo(line).value;
if (line.endsWith(',')) {
openName = false;
}
return;
}
// --------------- 获取继承 ---------------
if (openExtends === undefined && line.startsWith('extends:')) {
openExtends = true;
}
if (openExtends) {
content.extends = getInfo(line).value;
if (line.endsWith(',')) {
openExtends = false;
}
return;
}
// --------------- 获取 mixins ---------------
if (openMixins === undefined && line.startsWith('mixins:')) {
openMixins = true;
}
if (openMixins) {
content.mixins = getInfo(line).value;
if (line.endsWith(',')) {
openMixins = false;
}
return;
}
// --------------- 获取 editor ---------------
if (openEditors === undefined && line.startsWith('editor:')) {
openEditors = true;
return;
}
if (openEditors) {
if (line.endsWith('},')) {
openEditors = false;
return;
}
const info = getInfo(line);
content.editors[info.key] = info.value;
return;
}
}
// --------------- 获取 properties ---------------
if (openProperties === undefined && line.startsWith('properties:')) {
propTotalIndex = syncIndex(line, propTotalIndex);
if (propTotalIndex === 0) {
return;
}
openProperties = true;
return;
}
if (openProperties) {
if (openSubProp === undefined && line.includes('{')) {
subPropIndex = syncIndex(line, subPropIndex);
const info = getInfo(line);
subPropName = info.key;
content.properties[subPropName] = {
hasGet: undefined,
hasSet: undefined,
notify: undefined,
type: undefined,
default: undefined,
visible: undefined,
serializable: undefined,
content: '',
};
if (subPropIndex === 0) {
content.properties[subPropName].content = line;
return;
}
openSubProp = true;
return;
}
if (openSubProp) {
subPropIndex = syncIndex(line, subPropIndex);
if (subPropIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
openSubProp = undefined;
return;
}
const subProp = content.properties[subPropName];
subProp.content += (line + '\n');
if (hasGet === undefined && line.includes('get:')) {
getIndex = syncIndex(line, getIndex);
if (getIndex === 0) {
subProp.hasGet = noTrimLine + '\n';
return;
}
subProp.hasGet = ` get ${subPropName} () {\n`;
hasGet = true;
return;
}
if (hasGet) {
getIndex = syncIndex(line, getIndex);
if (getIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
hasGet = undefined;
subProp.hasGet += ' }';
return;
} else {
subProp.hasGet += ' ' + noTrimLine.substring(noTrimLine.search(/\S/), noTrimLine.length) + '\n';
}
return;
}
if (hasSet === undefined && line.includes('set:')) {
setIndex = syncIndex(line, setIndex);
let params = line.match(/(?<=\()(.*)(?=\))/);
params = params ? params[0].split(',') : [];
let str = '';
for (let i = 0; i < params.length; ++i) {
const param = params[i].trim();
if (param === '') {
continue;
}
if (i > 0) {
str += ' ';
}
str += `${param}: any`;
if (i < params.length - 1) {
str += ',';
}
}
if (setIndex === 0) {
subProp.hasSet = noTrimLine + '\n';
return;
}
subProp.hasSet = ` set ${subPropName} (${str}) {\n`;
hasSet = true;
return;
}
if (hasSet) {
setIndex = syncIndex(line, setIndex);
if (setIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
hasSet = undefined;
subProp.hasSet += ' }';
return;
} else {
subProp.hasSet += ' ' + noTrimLine.substring(noTrimLine.search(/\S/), noTrimLine.length) + '\n';
}
return;
}
if (hasNotify === undefined && line.includes('notify')) {
notifyIndex = syncIndex(line, notifyIndex);
if (notifyIndex === 0) {
subProp.notify = noTrimLine + '\n';
return;
}
hasNotify = true;
subProp.notify = line + '\n';
return;
}
if (hasNotify) {
notifyIndex = syncIndex(line, notifyIndex);
if (notifyIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
hasNotify = undefined;
subProp.notify += '}';
} else {
subProp.notify += line + '\n';
}
return;
}
const info = getInfo(line);
if (subProp.default === undefined && line.includes('default:')) {
subProp.default = info.value;
}
if (subProp.type === undefined && line.includes('type:')) {
subProp.type = info.value;
}
if (subProp.visible === undefined && line.includes('visible:')) {
subProp.visible = info.value;
}
if (subProp.serializable === undefined && line.includes('serializable:')) {
subProp.serializable = info.value;
}
} else {
propTotalIndex = syncIndex(line, propTotalIndex);
if (propTotalIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
openProperties = undefined;
return;
}
const info = getInfo(line);
let type = getType(info.value);
if (Array.isArray(type)) {
if (info.value.length > 2) {
type = 'array:' + info.value.substring(1, info.value.length - 1);
}
else {
type = undefined;
}
}
const value = info.value;
content.properties[info.key] = {
hasGet: undefined,
hasSet: undefined,
notify: undefined,
type: type,
default: value,
visible: undefined,
serializable: undefined,
content: line,
};
}
return;
}
// --------------- 获取 statics ---------------
if (openStatics === undefined && line.startsWith('statics:')) {
staticIndex = syncIndex(line, staticIndex);
if (staticIndex === 0) {
return;
}
openStatics = true;
return;
}
if (openStatics) {
staticIndex = syncIndex(line, staticIndex);
if (staticIndex === 0 && (line.endsWith('},') || line.endsWith(','))) {
openStatics = false;
subStaticName = undefined;
return;
}
if (subStaticName === undefined && line.includes('function')) {
const info = getInfo(line);
subStaticIndex = syncIndex(line, subStaticIndex);
let params = line.match(/(?<=\()(.*)(?=\))/);
params = params ? params[0].split(',') : [];
let str = '';
for (let i = 0; i < params.length; ++i) {
const param = params[i].trim();
if (param === '') {
continue;
}
if (i > 0) {
str += ' ';
}
str += `${param}: any`;
if (i < params.length - 1) {
str += ',';
}
}
if (subStaticIndex === 0) {
content.statics[info.key] = {
parameter: '',
content: noTrimLine + '\n',
};
return;
}
subStaticName = info.key;
content.statics[subStaticName] = {
parameter: params ? params[0] : '',
content: ` public static ${subPropName} (${str}) {\n`,
};
return;
}
if (subStaticName !== undefined) {
subStaticIndex = syncIndex(line, subStaticIndex);
if (subStaticIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
content.statics[subStaticName].content += ' }';
return;
}
content.statics[subStaticName].content += (' ' + noTrimLine.substring(noTrimLine.search(/\S/), noTrimLine.length) + '\n');
} else {
const info = getInfo(line);
content.statics[info.key] = {
parameter: '',
content: ` public static ${info.key} = ${info.value};\n`,
};
}
return;
}
// --------------- 获取函数 ---------------
if (openFunctions === undefined) {
const info = getInfo(line);
funcName = info.key;
let params = line.match(/(?<=\()(.*)(?=\))/);
if (params) {
params = params[0].split(',');
let str = '';
for (let i = 0; i < params.length; ++i) {
const param = params[i].trim();
if (param === '') {
continue;
}
if (i > 0) {
str += ' ';
}
str += `${param}: any`;
if (i < params.length - 1) {
str += ',';
}
}
content.functions[funcName] = {
parameter: params ? params[0] : '',
content: ` ${funcName} (${str}) {\n`,
};
funcIndex = syncIndex(line, funcIndex);
if (funcIndex === 0) {
content.functions[funcName].content += ' }\n\n';
return;
}
openFunctions = true;
} else {
content.properties[info.key] = {
hasGet: undefined,
hasSet: undefined,
notify: undefined,
type: undefined,
default: info.value,
visible: undefined,
serializable: undefined,
content: line,
};
return;
}
return;
}
if (openFunctions) {
funcIndex = syncIndex(line, funcIndex);
if (funcIndex === 0 && (line.endsWith('},') || line.endsWith('}'))) {
openFunctions = undefined;
content.functions[funcName].content += ' }\n\n';
return;
}
const func = content.functions[funcName];
const len = noTrimLine.search(/\S/);
func.content += `${noTrimLine.substring(0, len)}// ${noTrimLine.substring(len, noTrimLine.length)} \n`;
return;
}
}
}
catch (e) {
console.error(e);
}
});
return {
topNote,
ccKeys,
classCodeMap,
importCodeMap,
otherCodeMap,
endCodeMap,
};
}
function match(line: string, regExpStr: string, global: string = '') {
try {
const regExp = new RegExp(`(?<=${regExpStr})([a-zA-Z0-9]+)`, global);
const result = line.match(regExp);
if (result) {
if (!global) {
return result[result.length - 1];
} else {
let arr: string[] = [];
for (let element of result) {
arr.push(element);
}
return arr;
}
}
}
catch (e) {
console.error(e);
}
return null;
}
function getRegExp(str: string, global: string = '') {
return new RegExp(str, global);
}
function addCode(content: string, code: string, enter: boolean = true) {
if (code) {
content += code;
if (enter) {
content += '\n';
}
}
return content;
}
const RENAME_COMPONENT: any = {
'BoxCollider': 'BoxCollider2D',
'BoxCollider3D': 'BoxCollider',
'CircleCollider': 'CircleCollider2D',
'Collider': 'Collider2D',
'Collider3D': 'Collider',
'DistanceJoint': 'DistanceJoint2D',
'ClickEvent': 'EventHandler',
'MouseJoint': 'MouseJoint2D',
'WheelJoint': 'WheelJoint2D',
'PolygonCollider': 'PolygonCollider2D',
'ParticleSystem': 'ParticleSystem2D',
'ParticleSystem3D': 'ParticleSystem',
'Joint': 'Joint2D',
'RigidBody': 'RigidBody2D',
'RigidBody3D': 'RigidBody',
'SphereCollider3D': 'SphereCollider',
'RenderComponent': 'UIRenderable',
'SkeletonAnimation': 'SkeletalAnimation',
'Float': 'CCFloat',
'string': 'CCString',
'Boolean': 'CCBoolean',
'Integer': 'CCInteger',
};
export async function parseTSCode(baseClassName: string, path: string) {
let isTop = true;
let isOther = false;
let topCode = '';
let imports: string[] = ['_decorator'];
let decoratorCode = '';
let otherImportCode = '';
let otherDecoratorCode = '';
let cccclassCode = '';
let contentCode = '';
let openClass = false;
let waitOpenClass = false;// 需要检测到 { 才能开启 openClass
let classIndex = 0;
let openFunctions: boolean | undefined = undefined;
let funcIndex = 0;
let openConstructor: boolean | undefined = undefined;
let constructorIndex = 0;
function pushImports(name: string) {
name = RENAME_COMPONENT[name] || name;
if (!imports.includes(name)) {
imports.push(name);
}
return name;
}
function replaceCodeByClassName(line: string, noTrimLine: string, isFunc?: boolean) {
let classNames;
if (isFunc) {
// 删除只需要判断是否是 cc.xx
classNames = match(line, '\:? (cc\.)', 'g');
}
else {
classNames = match(line, '\:? ?(cc\.)', 'g');
}
if (classNames) {
let newline = noTrimLine;
for (let className of classNames) {
let newClassName = pushImports(className);
let RegExp = getRegExp(`cc.${className}`, 'g');
if (noTrimLine.trim().replace(/ /g, '').includes(`${className}=null`)) {
newline = noTrimLine.replace(RegExp, `${newClassName} | null`);
} else {
newline = noTrimLine.replace(RegExp, newClassName);
}
}
let matchArr = newline.match(/([a-zA-Z0-9]+)? =? ([a-zA-Z0-9]+)/);
if (matchArr && matchArr[1] !== undefined && (matchArr[1] === matchArr[2])) {
return undefined;
}
return newline + '\n';
} else if (noTrimLine) {
return noTrimLine + '\n';
}
}
await readWriteFileByLineWithProcess(path, (line: string) => {
try {
// 剔除空格
let noTrimLine = line;
line = line.trim();
if (!openClass) {
if (line.includes('export default class ') || line.includes('export class ') || waitOpenClass) {
// const name = match(line, 'class? ') as string;
// if (name) {
// line = line.replace(name, baseClassName);
// }
let extend = match(line, 'extends ?(cc\.)') as string;
if (extend) {
let newExtend = pushImports(extend);
contentCode += line.replace(`cc.${extend}`, newExtend);
}
else {
contentCode += line;
}
contentCode += '\n';
classIndex = syncIndex(line, classIndex);
if (classIndex === 0) {
waitOpenClass = true;
return;
}
waitOpenClass = false;
openClass = true;
}
else {
// 直接过滤注释文字
if (line.startsWith('/') || line.startsWith('*')) {
if (isTop) {
topCode += '// ' + noTrimLine + '\n';
}
else if (isOther) {
otherImportCode += '// ' + noTrimLine + '\n';
}
}
else if (line.includes('cc._decorator')) {
isTop = false;
decoratorCode += noTrimLine.replace(/cc._decorator/, '_decorator') + '\n';
}
else if (line.includes('@ccclass')) {
cccclassCode = noTrimLine;
}
else if (line.startsWith('@')) {
otherDecoratorCode += noTrimLine;
}
else {
isOther = true;
if (!line) {
return;
}
let newline = replaceCodeByClassName(line, noTrimLine);
if (newline === undefined) {
otherImportCode += `// ${noTrimLine}\n`;
}
else if (newline) {
otherImportCode += newline;
}
}
}
}
else {
// 直接过滤注释文字
if (line.startsWith('/') || line.startsWith('*')) {
contentCode += '//' + noTrimLine + '\n';
return;
}
// --------------- 检测是否解析类完毕 ---------------
classIndex = syncIndex(line, classIndex);
if (classIndex === 0 && (line.endsWith('}') || line.endsWith('};'))) {
contentCode += '}\n\n';
openClass = false;
return;
}
if (openFunctions === undefined) {
let newline = replaceCodeByClassName(line, noTrimLine);
if (newline !== undefined) {
if ((line.includes('constructor ()') || line.includes('constructor()'))) {
openConstructor = true;
}
contentCode += newline;
}
}
// 函数
if (openFunctions === undefined && line.match(/(?<=\()(.*)(?=\))/)) {
funcIndex = syncIndex(line, funcIndex);
if (funcIndex === 0) {
return;
}
openFunctions = true;
return;
}
if (openFunctions) {
funcIndex = syncIndex(line, funcIndex);
if (funcIndex === 0 && line.endsWith('}')) {
contentCode += noTrimLine;
contentCode += '\n';
openFunctions = undefined;
return;
}
if (line) {
if (openConstructor && line.startsWith('super();')) {
contentCode += ' ' + line;
}
else {
contentCode += ' // ' + line;
}
}
else {
contentCode += line;
}
contentCode += '\n';
return;
}
}
}
catch (e) {
console.error(e);
}
});
let content = '';
content = addCode(content, topCode);
let importCode = `import { `;
for (let i = 0; i < imports.length; ++i) {
importCode += imports[i];
if (i < imports.length - 1) {
importCode += ', ';
}
}
importCode += ` } from 'cc';`;
content = addCode(content, importCode);
content = addCode(content, decoratorCode);
content = addCode(content, otherImportCode);
content = addCode(content, cccclassCode.replace(/@ccclass/, `@ccclass('${baseClassName}')`));
content = addCode(content, otherDecoratorCode);
// content = addCode(content, exportClassCode);
content = addCode(content, contentCode);
return {
content: content,
};
}

View File

@@ -0,0 +1,846 @@
'use strict';
// @ts-ignore
import { readdirSync, statSync, readJSONSync, readFileSync, ensureDirSync, copyFileSync, existsSync } from 'fs-extra';
import { join, dirname, basename, relative, extname } from 'path';
import { ImporterBase } from "./base";
import { createReadStream } from 'fs';
import { createInterface } from 'readline';
// @ts-ignore
import { DOMParser } from "xmldom";
import { createHash } from 'crypto';
const lodash = require('lodash');
export const SKIPS_SCRIPT = [
'use_reversed_rotateBy.js',
'use_reversed_rotateTo.js',
'use_v2.0.x_cc.Toggle_event.js',
'use_v2.1-2.2.1_cc.Toggle_event.js',
];
// 优化原本的 localeCompare 方法性能提升1000 空节点 1103ms -> 31ms
export const collator = new Intl.Collator('en', {
numeric: true,
sensitivity: 'base',
});
// 2d 的分组
export let layerToGroupMap: Map<number, number> = new Map();
let groupList: string[] = [];
export async function initGroupList(path: string) {
try {
const project = readJSONSync(join(path, '../settings/project.json'));
groupList = project['group-list'] || [];
for (let i = 0; i < groupList.length; ++i) {
let group = groupList[i];
await setGroupLayerByIndex(i, group);
}
}
catch (e) {
groupList = [];
// console.log(e);
}
}
export async function getGroupLayerByIndex(index: number) {
if (groupList.length === 0) {
return 1 << 25; // 默认为 UI_2D
}
const group = groupList[index];
if (group) {
return await setGroupLayerByIndex(index, group);
}
else {
return null;
}
}
export async function setGroupLayerByIndex(index: number, group: string) {
let userLayers = await Editor.Profile.getProject('project', 'layer');
let layer;
if (!userLayers) {
userLayers = [];
} else {
if (group === 'default') {
group = 'Default';
}
layer = userLayers.find((layer: any) => layer.name === group);
}
if (!layer) {
const length = userLayers.length;
layer = {
name: group,
value: (1 << length),
};
userLayers.push(layer);
// console.log('layer: ' + layer.name + ' ' + layer.value);
let key = 1 << index;
layerToGroupMap.set(key, layer.value);
await Editor.Profile.setProject('project', 'layer', userLayers);
}
return layer.value;
}
// 替换 fbx sub meta 中的 uuid
export const replaceFbxUuidMap: Map<string, any> = new Map<string, any>();
// 存储导入项目所有资源
export const importProjectAssets: Map<string, any> = new Map<string, any>();
export const importSubAssets: Map<string, any> = new Map<string, any>();
// 存储 uuid 列表,处理 uuid 冲突,确保 uuid 都是唯一的
export const uuidList: Map<string, string> = new Map<string, string>();
// 脚本名
export const scriptList: Map<string, string> = new Map<string, string>();
export let replaceScriptList = [];
export function updateReplaceScriptList(list: []) {
replaceScriptList = list;
}
export function saveUuid(oldUuid: string, newUuid: string) {
uuidList.set(oldUuid, newUuid);
}
export function getNewUuid(oldUuid: string) {
let uuid = uuidList.get(oldUuid);
if (!uuid) {
let info = importProjectAssets.get(oldUuid);
if (info && info.outUuid) {
uuid = info.outUuid;
}
if (!uuid) {
info = importSubAssets.get(oldUuid);
if (info && info.uuid) {
uuid = info.uuid;
}
}
}
return uuid || oldUuid;
}
export function clear() {
scriptList.clear();
replaceScriptList.length = 0;
replaceFbxUuidMap.clear();
}
// 存储 2D 默认资源的信息列表
interface DefaultAssets2D {
type: string,
path: string,
baseUuid: string,
}
const defaultAssets2DList: Map<string, DefaultAssets2D> = new Map<string, DefaultAssets2D>();
export function getDefaultAssets2D(uuid: string) {
return defaultAssets2DList.get(uuid);
}
export function scanningDefaultAssets2D() {
const default_asset_root = join(__dirname, '../../static');
const rootPath = join(__dirname, '../../static/migrate-resources/default-assets-2d');
defaultAssets2DList.clear();
function step(path: string) {
try {
if (path.endsWith('.DS_Store')) {
return;
}
const stat = statSync(path);
if (stat.isDirectory()) {
const names = readdirSync(path);
names.forEach((name: string) => {
const tempPath = join(path, name);
if (name.endsWith('.meta')) {
addImportProjectAssets(default_asset_root, tempPath, true);
return;
}
step(tempPath);
});
}
else {
const metaPath = join(dirname(path), basename(path) + '.meta');
const meta = readJSONSync(metaPath);
defaultAssets2DList.set(meta.uuid, {
path: path,
type: meta.type,
baseUuid: meta.uuid,
});
for (const key in meta.subMetas) {
const subMeta = meta.subMetas[key];
if (subMeta) {
defaultAssets2DList.set(subMeta.uuid, {
path: path,
type: meta.type,
baseUuid: meta.uuid,
});
}
}
}
} catch(error) {
console.error(error);
}
}
step(rootPath);
}
export function isFbxMultKey(subMetas: any, key: string) {
if (key.includes('-')) {
const elements = key.split('-');
const modeName = elements[0];
const keys = Object.keys(subMetas).map((key) => {
return key.includes(modeName + '-');
}).filter(Boolean);
return keys.length > 1;
}
return false;
}
export function addImportProjectAssets(root: string, path: string, isDefaultAssets: boolean = false) {
try {
let base = path.replace('.meta', '');
if (base.endsWith('.fire')) {
base = base.replace(/.fire+$/g, '.scene');
} else if (base.endsWith('.js')) {
base = base.replace(/.js+$/g, '.ts');
}
const meta = readJSONSync(path);
let outPath;
if (isDefaultAssets) {
outPath = join(Editor.Project.path, 'assets', relative(root, base + '.meta'));
}
else {
outPath = join(Editor.Project.path, relative(root, base + '.meta'));
}
if (existsSync(outPath)) {
const type = extname(base);
if (type === '.fbx' || type === '.FBX') {
for (let key in meta.subMetas) {
const subMeta = meta.subMetas[key];
if (subMeta) {
const isMult = isFbxMultKey(meta.subMetas, key);
// console.log('修改前:' + key + ' ' + isMult);
key = getFBXSubMetaNewName(path.replace('.meta', ''), key, isMult);
// console.log('修改后:' + key + ' ' + ImporterBase.getNameByID(key));
importSubAssets.set(subMeta.uuid, {
baseUuid: meta.uuid,
uuid: `${meta.uuid}@${ImporterBase.getNameByID(key)}`,
});
}
}
}
else if (meta.type === 'Texture Packer') {
for (let key in meta.subMetas) {
const subMeta = meta.subMetas[key];
if (subMeta) {
key = basename(key, extname(key));
importSubAssets.set(subMeta.uuid, {
baseUuid: meta.uuid,
uuid: `${meta.uuid}@${ImporterBase.getNameByID(key)}`,
});
}
}
}
else if (meta.type === 'sprite') {
for (const key in meta.subMetas) {
const subMeta = meta.subMetas[key];
if (subMeta) {
importSubAssets.set(subMeta.uuid, {
baseUuid: meta.uuid,
uuid: `${meta.uuid}@${ImporterBase.getNameByID('spriteFrame')}`,
});
}
}
}
else if (meta.type === 'raw') {
importSubAssets.set(meta.uuid, {
baseUuid: meta.uuid,
uuid: `${meta.uuid}@${ImporterBase.getNameByID('texture')}`,
});
}
importProjectAssets.set(meta.uuid, {
type: type,
basePath: path,
outPath: outPath,
outUuid: meta.uuid,
meta: meta,
});
}
}
catch (e) {}
}
export let chunksCacheBy2D: Map<string, any> = new Map<string, any>();
const getChunks = (path: string, regexp: RegExp, extname: string) => {
const chunksMap: Map<string, any> = new Map<string, any>();
function step(dir: string) {
const names = readdirSync(dir);
names.forEach((name: string) => {
const file = join(dir, name);
if (regexp.test(name)) {
const name = basename(file, extname);
const content = readFileSync(file, { encoding: 'utf8' });
chunksMap.set(name, {
from: file, // 源文件
to: join(Editor.Project.path, 'assets', 'migrate-resources', 'chunks', name + '.chunk'), // 导入到项目到
content: content,
getIncludePath(effectPath: string) {
return relative(effectPath, this.to);
},
});
} else if (statSync(file).isDirectory()) {
step(file);
}
});
}
step(path);
return chunksMap;
};
export function init2DChunks() {
chunksCacheBy2D = getChunks(join(__dirname, '../../static/migrate-resources/chunks'), /\.inc$/, '.inc');
}
export function import2DChunks(noRefres?: boolean): Promise<boolean> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
let idx = 0;
let open = false;
for (const [key, value] of chunksCacheBy2D) {
if (!existsSync(value.to)) {
// console.log('导入:' + value.to);
ensureDirSync(dirname(value.to));
copyFileSync(value.from, value.to);
open = true;
}
idx++;
if (idx >= chunksCacheBy2D.size) {
if (open && !noRefres) {
await Editor.Message.request('asset-db', 'refresh-asset', 'db://assets');
}
resolve(true);
}
}
});
}
/* comporess texture */
const migrateMap: Record<string, string> = {
pvrtc_4bits: 'pvrtc_4bits_rgba',
pvrtc_2bits: 'pvrtc_2bits_rgba',
etc2: 'etc2_rgba',
etc1: 'etc1_rgb_a',
};
const PLATFORMS = ['miniGame', 'web', 'android', 'ios', 'pc'];
export async function migratePlatformSettings(platformSettings: any) {
if (!platformSettings || Object.keys(platformSettings).length === 0) {
return;
}
const result = {
useCompressTexture: true,
presetId: '',
};
if (platformSettings.default && Object.keys(platformSettings).length === 1) {
// 只有默认配置需要全部平台都配一遍
PLATFORMS.forEach((platformType) => {
const config = {};
platformSettings.default.formats.forEach((format: any) => {
// @ts-ignore
config[format.name] = format.quality;
});
platformSettings[platformType] = config;
});
} else {
Object.keys(platformSettings).forEach((platformType) => {
if (platformType === 'default') {
return;
}
if (platformType !== 'default') {
const defaultConfig: any = {};
if (platformSettings.default) {
const defaultData = JSON.parse(JSON.stringify(platformSettings.default));
if (defaultData.formats) {
defaultData.formats.forEach((format: any) => {
defaultConfig[format.name] = format.quality;
});
}
}
const otherConfig: any = {};
platformSettings[platformType].formats.forEach((format: any) => {
otherConfig[format.name] = format.quality;
});
platformSettings[platformType] = Object.assign(defaultConfig, otherConfig);
}
migrateCompressTextureType(platformSettings[platformType]);
if (platformType === 'minigame') {
platformSettings.miniGame = platformSettings.minigame;
delete platformSettings.minigame;
}
});
}
delete platformSettings.default;
if (Object.keys(platformSettings).length === 0) {
return;
}
result.presetId = await getPresetId(platformSettings);
return result;
}
function migrateCompressTextureType(config: any) {
if (!config) {
return;
}
Object.keys(config).forEach((name: string) => {
if (!migrateMap[name]) {
return;
}
config[migrateMap[name]] = config[name];
delete config[name];
});
}
async function getPresetId(platformSettings: any) {
const presetId = 'presetId' + Date.now();
// @ts-ignore
let userPreset = await Editor.Profile.getProject('builder', 'textureCompressConfig.userPreset');
if (!userPreset) {
userPreset = {
[presetId]: {
name: presetId,
options: platformSettings,
},
};
// @ts-ignore
await Editor.Profile.setProject('builder', `textureCompressConfig.userPreset`, userPreset);
return presetId;
}
for (const Id of Object.keys(userPreset)) {
if (lodash.isEqual(userPreset[Id].options, platformSettings)) {
return Id;
}
}
// @ts-ignore
await Editor.Profile.setProject('builder', `textureCompressConfig.userPreset.${presetId}`, {
name: presetId,
options: platformSettings,
});
return presetId;
}
export function getBlendFactor2DTo3D(value: number) {
switch (value) {
case 0: // ZERO
return 0;
case 1: // ONE
return 1;
case 0x302:// SRC_ALPHA
return 2;
case 0x304:// DST_ALPHA
return 3;
case 0x303:// ONE_MINUS_SRC_ALPHA
return 4;
case 0x305:// ONE_MINUS_DST_ALPHA
return 5;
case 0x300:// SRC_COLOR
return 6;
case 0x306:// DST_COLOR
return 7;
case 0x301:// ONE_MINUS_SRC_COLOR
return 8;
case 0x307:// ONE_MINUS_DST_COLOR
return 9;
}
return value;
}
export function hasComponent(target: any, json3D: any, type: string) {
for (const component of target._components) {
const id = component.__id__;
if (json3D[id].__type__ === type) {
return true;
}
}
return false;
}
const UI_COMPONENT = [
'cc.Canvas',
'cc.Widget',
'cc.Sprite',
'cc.Label',
'cc.LabelOutline',
'cc.LabelShadow',
'cc.RichText',
'cc.ParticleSystem',
'cc.TiledMap',
'cc.TiledTile',
'cc.TiledLayer',
'cc.TiledObjectGroup',
'cc.Layout',
'cc.Button',
'cc.ScrollView',
'cc.Slider',
'cc.PageView',
'cc.ProgressBar',
'cc.Toggle',
'cc.ToggleContainer',
'cc.ToggleGroup',
'cc.EditBox',
'cc.VideoPlayer',
'cc.WebView',
'cc.UITransform',
'cc.UIOpacity',
'sp.Skeleton',
'dragonBones.ArmatureDisplay',
];
export function hasUIRenderComponent(target: any, json: any) {
if (!target._is3DNode) {
return true;
}
// 如果是自动同步的 prefab 是没有 _components 的
if (!target._components) {
return false;
}
for (const componentData of target._components) {
const id = componentData.__id__;
const component = json[id];
if (component) {
const __type__ = component.__type__;
if (UI_COMPONENT.includes(__type__)) {
return true;
}
}
}
for (const childData of target._children) {
const id = childData.__id__;
const child = json[id];
if (hasUIRenderComponent(child, json)) {
return true;
}
}
return false;
}
export function hasCanvasComponent(node: any, json2D: any) {
for (const componentData of node._components) {
const id = componentData.__id__;
const component = json2D[id];
if (component) {
const __type__ = component.__type__;
if (__type__ === 'cc.Canvas') {
return true;
}
}
}
return false;
}
export function setColor(uiComponent: any, nodeID: any, json2D: any) {
if (nodeID) {
const node = json2D[nodeID];
if (node && node._color) {
uiComponent._color.r = node._color.r;
uiComponent._color.g = node._color.g;
uiComponent._color.b = node._color.b;
}
}
}
export function getFBXSubMetaNewName(fsPath: string, baseName: string, isMult: boolean) {
let ext = extname(baseName);
const elements = baseName.split('-');
let name = elements && elements[0];
const modelName = basename(fsPath, extname(fsPath));
if (name && (modelName === name)) {
switch (ext) {
case '.sac':
name = `UnnamedAnimation`;
break;
case '.image':
name = `UnnamedImage`;
break;
case '.mesh':
name = `UnnamedMesh`;
break;
case '.mtl':
name = `UnnamedMaterial`;
break;
case '.skeleton':
name = `UnnamedSkeleton`;
break;
case '.texture':
name = `UnnamedTexture`;
break;
default:
name = `Unnamed`;
}
if (isMult) {
name = name + '-' + elements[1];
}
}
name = name.replace(ext, '');
switch (ext) {
case '.sac':
ext = '.animation';
break;
case '.mtl':
ext = '.material';
break;
}
return name + ext;
}
export async function readWriteFileByLineWithProcess(readName: any, callback: any) {
await new Promise((resolve) => {
const readStream = createReadStream(readName);
const readLine = createInterface({
input: readStream,
});
readLine.on('line', (line: string) => {
callback(line);
});
readLine.on('close', () => {
resolve(true);
});
});
}
/**
* 读取 tmx 文件内容,查找依赖的 texture 文件信息
* @param tmxFile tmx 文件路径
* @param tmxFileData tmx 文件内容
* @returns imageFullPaths
*/
export async function searchTmxDependImages(tmxFile: string, tmxFileData: string) {
// 读取 xml 数据
const doc = new DOMParser().parseFromString(tmxFileData);
if (!doc) {
console.error(`TiledMap import failed: failed to parser ${tmxFile}`);
return;
}
let imgFullPaths: string[] = [];
const rootElement = doc.documentElement;
const tilesetElements = rootElement.getElementsByTagName('tileset');
// 读取内部的 source 数据
for (let i = 0; i < tilesetElements.length; i++) {
const tileset = tilesetElements[i];
const sourceTSXAttr = tileset.getAttribute('source');
if (sourceTSXAttr) {
// 获取 texture 路径
const tsxAbsPath = join(dirname(tmxFile), sourceTSXAttr);
if (existsSync(tsxAbsPath)) {
const tsxContent = readFileSync(tsxAbsPath, 'utf-8');
const tsxDoc = new DOMParser().parseFromString(tsxContent);
if (tsxDoc) {
const imageFullPath = await parseTilesetImages(tsxDoc, tsxAbsPath);
imgFullPaths = imgFullPaths.concat(imageFullPath);
} else {
console.warn('Parse %s failed.', tsxAbsPath);
}
}
}
// import images
const imageFullPath = await parseTilesetImages(tileset, tmxFile);
imgFullPaths = imgFullPaths.concat(imageFullPath);
}
const imageLayerTextures: string[] = [];
const imageLayerElements = rootElement.getElementsByTagName('imagelayer');
for (let ii = 0, nn = imageLayerElements.length; ii < nn; ii++) {
const imageLayer = imageLayerElements[ii];
const imageInfos = imageLayer.getElementsByTagName('image');
if (imageInfos && imageInfos.length > 0) {
const imageInfo = imageInfos[0];
const imageSource = imageInfo.getAttribute('source');
const imgPath = join(dirname(tmxFile), imageSource!);
if (existsSync(imgPath)) {
imageLayerTextures.push(imgPath);
} else {
console.warn('Parse %s failed.', imgPath);
}
}
}
return imgFullPaths.concat(imageLayerTextures);
}
/**
* 读取文件路径下 image 的 source 路径信息以及对应的文件名
* @param tsxDoc
* @param tsxPath
* @returns imageFullPath
*/
export async function parseTilesetImages(tsxDoc: Element | Document, tsxPath: string) {
const images = tsxDoc.getElementsByTagName('image');
const imageFullPath: string[] = [];
for (let i = 0; i < images.length; i++) {
const image = images[i];
const imageCfg = image.getAttribute('source');
if (imageCfg) {
const imgPath = join(dirname(tsxPath), imageCfg);
imageFullPath.push(imgPath);
}
}
return imageFullPath;
}
export function getColor(node: any) {
if (node && node._color) {
return {
"__type__": "cc.Color",
"r": node._color.r,
"g": node._color.g,
"b": node._color.b,
"a": node._color.a,
};
}
}
const halfToRad = 0.5 * Math.PI / 180.0;
export function fromEuler(out: any, x: number, y: number, z: number) {
x *= halfToRad;
y *= halfToRad;
z *= halfToRad;
const sx = Math.sin(x);
const cx = Math.cos(x);
const sy = Math.sin(y);
const cy = Math.cos(y);
const sz = Math.sin(z);
const cz = Math.cos(z);
out.x = sx * cy * cz + cx * sy * sz;
out.y = cx * sy * cz + sx * cy * sz;
out.z = cx * cy * sz - sx * sy * cz;
out.w = cx * cy * cz - sx * sy * sz;
return out;
}
/**
* 项目内的脚本文件名称不能重复
*/
export const scriptName = {
allScripts: null,
allClassNames: [],
timer: 0,
fileName: '',
className: '',
async isValid(fileName: string) {
const className = this.getValidClassName(fileName);
if (!className) {
return { state: 'errorScriptClassName' };
}
const exist = await Editor.Message.request('scene', 'query-component-has-script', className);
if (!exist) {
return { state: '' };
}
return { state: 'errorScriptClassNameExist', message: className };
},
async getValidFileName(fileName: string) {
fileName = fileName.trim().replace(/[^a-zA-Z0-9_-]/g, '');
const baseName = fileName;
let index = 0;
while ((await this.isValid(fileName)).state) {
index++;
const padString = `-${index.toString().padStart(3, '0')}`;
fileName = `${baseName}${padString}`;
}
return fileName;
},
getValidClassName(fileName: string) {
/**
* 类名转为大驼峰格式:
* 头部不能有数字
* 不含特殊字符
* 符号和空格作为间隔,每个间隔后的首字母大写,如:
* 0my class_name-for#demo! 转后为 MyClassNameForDemo
*/
fileName = fileName.trim().replace(/^[^a-zA-Z]+/g, '');
const parts = fileName.match(/[a-zA-Z0-9]+/g);
if (parts) {
return parts
.filter(Boolean)
.map((part) => part[0].toLocaleUpperCase() + part.substr(1))
.join('');
}
return '';
},
};
// 排序
export function sizeSorting(a: any, b: any) {
const aID = a.__id__;
const bID = b.__id__;
return bID - aID;
}
// 比对版本号
export function compareVersion(versionA: string, versionB: string) {
const a = versionA.split('.');
const b = versionB.split('.');
const length = Math.max(a.length, b.length);
for (let i = 0; i < length; i++) {
const an = a[i] || 0;
const bn = b[i] || 0;
if (Number(an) < Number(bn)) {
return -1;
}
if (Number(an) > Number(bn)) {
return 1;
}
}
return 0;
}
export function getComponentByType(nodeID: number, componentType: string, json: any) {
const node = json[nodeID];
const components = node._components.map((component: any) => json[component.__id__]);
return components.find((component: any) => component.__type__ === componentType);
}
export async function getDesignResolution() {
const width = await Editor.Profile.getProject('project', 'general.designResolution.width');
const height = await Editor.Profile.getProject('project', 'general.designResolution.height');
return {
width: width || 960,
height: height || 640,
}
}
const _extendIndex = [
1, 2, 3, 4, 5,
7, 8, 9, 10, 11, 12, 13, 14, 15,
17, 18, 19, 20, 21, 22, 23, 24,
26, 27, 28, 29, 30
];
export function nameToId(name: string, extend?: number) {
if (!extend) {
extend = 0;
}
const md5 = createHash('md5').update(name).digest('hex');
let id = md5[0] + md5[6] + md5[16] + md5[25] + md5[31];
for (let i = 0; i < extend; i++) {
id += md5[_extendIndex[i]];
}
return id;
}

View File

@@ -0,0 +1,29 @@
'use strict';
export const ALPHAKEY = {
"__type__": "cc.AlphaKey",
"alpha": 1,
"time": 0,
};
export class AlphaKey {
static create() {
return JSON.parse(JSON.stringify(ALPHAKEY));
}
static async migrate(json2D: any) {
const source = JSON.parse(JSON.stringify(ALPHAKEY));
for (const key in json2D) {
const value = json2D[key];
if (key === '__type__' || value === undefined || value === null) { continue; }
source[key] = value;
}
return source;
}
static async apply(index: number, json2D: any, json3D: any) {
const source = await AlphaKey.migrate(json2D[index]);
json3D.splice(index, 1, source);
return source;
}
}

View File

@@ -0,0 +1,35 @@
'use strict';
export const ANIMATION = {
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"node": null,
"_enabled": true,
"__prefab": null,
"playOnLoad": false,
"_clips": [],
"_defaultClip": null,
};
export class Animation {
static create() {
return JSON.parse(JSON.stringify(ANIMATION));
}
static async migrate(json2D: any) {
const source = JSON.parse(JSON.stringify(ANIMATION));
for (const key in json2D) {
const value = json2D[key];
if (key === '__type__' || value === undefined || value === null) { continue; }
source[key] = value;
}
return source;
}
static async apply(index: number, json2D: any, json3D: any) {
const source = await Animation.migrate(json2D[index]);
json3D.splice(index, 1, source);
return source;
}
}

View File

@@ -0,0 +1,40 @@
'use strict';
export const ANIMATIONCLIP = {
"__type__": "cc.AnimationClip",
"_name": "",
"_objFlags": 0,
"_native": "",
"sample": 60,
"speed": 1,
"wrapMode": 1,
"events": [],
"_duration": 0,
"_keys": [],
"_stepness": 0,
"_curves": [],
"_commonTargets": [],
"_hash": 0,
};
export class AnimationClip {
static create() {
return JSON.parse(JSON.stringify(ANIMATIONCLIP));
}
static async migrate(json2D: any) {
const source = JSON.parse(JSON.stringify(ANIMATIONCLIP));
for (const key in json2D) {
const value = json2D[key];
if (key === '__type__' || value === undefined || value === null) { continue; }
source[key] = value;
}
return source;
}
static async apply(index: number, json2D: any, json3D: any) {
const source = await AnimationClip.migrate(json2D[index]);
json3D.splice(index, 1, source);
return source;
}
}

Some files were not shown because too many files have changed in this diff Show More