@localizeaso/cli 0.1.0-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +35 -0
- package/packages/asc-shared/dist/app-store-review.d.ts +610 -0
- package/packages/asc-shared/dist/app-store-review.d.ts.map +1 -0
- package/packages/asc-shared/dist/app-store-review.js +242 -0
- package/packages/asc-shared/dist/aso-keyword-map.d.ts +94 -0
- package/packages/asc-shared/dist/aso-keyword-map.d.ts.map +1 -0
- package/packages/asc-shared/dist/aso-keyword-map.js +292 -0
- package/packages/asc-shared/dist/constants.d.ts +15 -0
- package/packages/asc-shared/dist/constants.d.ts.map +1 -0
- package/packages/asc-shared/dist/constants.js +130 -0
- package/packages/asc-shared/dist/cross-localization.d.ts +29 -0
- package/packages/asc-shared/dist/cross-localization.d.ts.map +1 -0
- package/packages/asc-shared/dist/cross-localization.js +189 -0
- package/packages/asc-shared/dist/dedupe.d.ts +17 -0
- package/packages/asc-shared/dist/dedupe.d.ts.map +1 -0
- package/packages/asc-shared/dist/dedupe.js +104 -0
- package/packages/asc-shared/dist/design-tokens.d.ts +83 -0
- package/packages/asc-shared/dist/design-tokens.d.ts.map +1 -0
- package/packages/asc-shared/dist/design-tokens.js +73 -0
- package/packages/asc-shared/dist/index.d.ts +16 -0
- package/packages/asc-shared/dist/index.d.ts.map +1 -0
- package/packages/asc-shared/dist/index.js +16 -0
- package/packages/asc-shared/dist/keywords.d.ts +48 -0
- package/packages/asc-shared/dist/keywords.d.ts.map +1 -0
- package/packages/asc-shared/dist/keywords.js +376 -0
- package/packages/asc-shared/dist/limits.d.ts +11 -0
- package/packages/asc-shared/dist/limits.d.ts.map +1 -0
- package/packages/asc-shared/dist/limits.js +9 -0
- package/packages/asc-shared/dist/locales.d.ts +10 -0
- package/packages/asc-shared/dist/locales.d.ts.map +1 -0
- package/packages/asc-shared/dist/locales.js +314 -0
- package/packages/asc-shared/dist/monetization-boundary.d.ts +148 -0
- package/packages/asc-shared/dist/monetization-boundary.d.ts.map +1 -0
- package/packages/asc-shared/dist/monetization-boundary.js +365 -0
- package/packages/asc-shared/dist/post-approval-paths.d.ts +30 -0
- package/packages/asc-shared/dist/post-approval-paths.d.ts.map +1 -0
- package/packages/asc-shared/dist/post-approval-paths.js +25 -0
- package/packages/asc-shared/dist/review-gate-summary.d.ts +166 -0
- package/packages/asc-shared/dist/review-gate-summary.d.ts.map +1 -0
- package/packages/asc-shared/dist/review-gate-summary.js +354 -0
- package/packages/asc-shared/dist/reviewer-feedback.d.ts +19 -0
- package/packages/asc-shared/dist/reviewer-feedback.d.ts.map +1 -0
- package/packages/asc-shared/dist/reviewer-feedback.js +94 -0
- package/packages/asc-shared/dist/screenshot-review.d.ts +478 -0
- package/packages/asc-shared/dist/screenshot-review.d.ts.map +1 -0
- package/packages/asc-shared/dist/screenshot-review.js +17 -0
- package/packages/asc-shared/dist/supabase.types.d.ts +541 -0
- package/packages/asc-shared/dist/supabase.types.d.ts.map +1 -0
- package/packages/asc-shared/dist/supabase.types.js +5 -0
- package/packages/asc-shared/dist/validation.d.ts +42 -0
- package/packages/asc-shared/dist/validation.d.ts.map +1 -0
- package/packages/asc-shared/dist/validation.js +113 -0
- package/scripts/ensure-shared-build.mjs +76 -0
- package/scripts/export-astro-mcp-apps.mjs +841 -0
- package/scripts/localizeaso.mjs +2100 -0
- package/scripts/review-agent.mjs +9092 -0
- package/scripts/review-mcp.mjs +5931 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function cleanString(value) {
|
|
5
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
6
|
+
}
|
|
7
|
+
function normalizeLocaleKey(value) {
|
|
8
|
+
return value.trim().replace(/_/g, '-');
|
|
9
|
+
}
|
|
10
|
+
function normalizeKeyword(value) {
|
|
11
|
+
return String(value ?? '')
|
|
12
|
+
.normalize('NFKD')
|
|
13
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/\s+/g, ' ')
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
function finiteRoundedNumber(value) {
|
|
19
|
+
return typeof value === 'number' && Number.isFinite(value) ? Math.round(value) : undefined;
|
|
20
|
+
}
|
|
21
|
+
function normalizeKeywordContext(value) {
|
|
22
|
+
if (!isRecord(value))
|
|
23
|
+
return { sources: [], keywords: {}, rows: [] };
|
|
24
|
+
const sources = Array.isArray(value.sources)
|
|
25
|
+
? Array.from(new Set(value.sources.map(cleanString).filter(Boolean)))
|
|
26
|
+
: [];
|
|
27
|
+
const keywords = {};
|
|
28
|
+
if (isRecord(value.keywords)) {
|
|
29
|
+
for (const [locale, rawKeywords] of Object.entries(value.keywords)) {
|
|
30
|
+
if (!Array.isArray(rawKeywords))
|
|
31
|
+
continue;
|
|
32
|
+
const normalizedLocale = normalizeLocaleKey(locale);
|
|
33
|
+
keywords[normalizedLocale] = Array.from(new Set(rawKeywords.map(cleanString).filter(Boolean)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const rows = [];
|
|
37
|
+
if (Array.isArray(value.rows)) {
|
|
38
|
+
for (const row of value.rows) {
|
|
39
|
+
if (!isRecord(row))
|
|
40
|
+
continue;
|
|
41
|
+
const locale = normalizeLocaleKey(cleanString(row.locale));
|
|
42
|
+
const keyword = cleanString(row.keyword);
|
|
43
|
+
if (!locale || !keyword)
|
|
44
|
+
continue;
|
|
45
|
+
rows.push({
|
|
46
|
+
locale,
|
|
47
|
+
keyword,
|
|
48
|
+
normalizedKeyword: normalizeKeyword(keyword),
|
|
49
|
+
popularity: finiteRoundedNumber(row.popularity),
|
|
50
|
+
difficulty: finiteRoundedNumber(row.difficulty),
|
|
51
|
+
isPreferred: typeof row.isPreferred === 'boolean' ? row.isPreferred : undefined,
|
|
52
|
+
source: cleanString(row.source) || undefined,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const [locale, localeKeywords] of Object.entries(keywords)) {
|
|
57
|
+
for (const keyword of localeKeywords) {
|
|
58
|
+
const normalizedKeyword = normalizeKeyword(keyword);
|
|
59
|
+
if (rows.some((row) => row.locale === locale && row.normalizedKeyword === normalizedKeyword)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
rows.push({ locale, keyword, normalizedKeyword });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { sources, keywords, rows };
|
|
66
|
+
}
|
|
67
|
+
function fieldIndexedForAppleSearch(field) {
|
|
68
|
+
const normalizedField = field.toLowerCase();
|
|
69
|
+
return ['name', 'title', 'subtitle', 'keywords', 'keyword', 'keywordfield', 'screenshotcaption'].includes(normalizedField.replace(/[^a-z]/g, ''));
|
|
70
|
+
}
|
|
71
|
+
function indexedFieldNote(field) {
|
|
72
|
+
return fieldIndexedForAppleSearch(field)
|
|
73
|
+
? 'Indexed by Apple search signals.'
|
|
74
|
+
: 'Not a primary Apple search keyword field; use as conversion/context signal.';
|
|
75
|
+
}
|
|
76
|
+
function matchKeywordInValue(value, keyword) {
|
|
77
|
+
const normalizedKeyword = normalizeKeyword(keyword);
|
|
78
|
+
if (!normalizedKeyword)
|
|
79
|
+
return [];
|
|
80
|
+
const normalizedValue = normalizeKeyword(value);
|
|
81
|
+
const positions = [];
|
|
82
|
+
let fromIndex = 0;
|
|
83
|
+
while (fromIndex < normalizedValue.length) {
|
|
84
|
+
const matchIndex = normalizedValue.indexOf(normalizedKeyword, fromIndex);
|
|
85
|
+
if (matchIndex === -1)
|
|
86
|
+
break;
|
|
87
|
+
positions.push({
|
|
88
|
+
start: matchIndex,
|
|
89
|
+
end: matchIndex + normalizedKeyword.length,
|
|
90
|
+
text: value.slice(matchIndex, matchIndex + normalizedKeyword.length),
|
|
91
|
+
});
|
|
92
|
+
fromIndex = matchIndex + Math.max(1, normalizedKeyword.length);
|
|
93
|
+
}
|
|
94
|
+
return positions;
|
|
95
|
+
}
|
|
96
|
+
function issue(params) {
|
|
97
|
+
return {
|
|
98
|
+
severity: params.severity ?? 'warning',
|
|
99
|
+
code: params.code,
|
|
100
|
+
message: params.message,
|
|
101
|
+
locale: params.locale,
|
|
102
|
+
field: params.field,
|
|
103
|
+
keyword: params.keyword,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function buildAsoKeywordDetectionReport(input) {
|
|
107
|
+
const keywordContext = normalizeKeywordContext(input.keywordContext);
|
|
108
|
+
const locales = Array.from(new Set([
|
|
109
|
+
...Object.keys(input.metadataByLocale).map(normalizeLocaleKey),
|
|
110
|
+
...keywordContext.rows.map((row) => row.locale),
|
|
111
|
+
...Object.keys(keywordContext.keywords).map(normalizeLocaleKey),
|
|
112
|
+
]))
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.sort();
|
|
115
|
+
const globalWarnings = [];
|
|
116
|
+
const globalErrors = [];
|
|
117
|
+
if (!keywordContext.rows.length) {
|
|
118
|
+
globalWarnings.push(issue({
|
|
119
|
+
code: 'keyword_context_missing',
|
|
120
|
+
message: 'No keyword context rows are attached; ASO keyword detection can only report metadata fields.',
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
const localeReports = locales.map((locale) => {
|
|
124
|
+
const metadataFields = input.metadataByLocale[locale] ?? {};
|
|
125
|
+
const keywords = keywordContext.rows.filter((row) => row.locale === locale);
|
|
126
|
+
const fieldReports = [];
|
|
127
|
+
const warnings = [];
|
|
128
|
+
const errors = [];
|
|
129
|
+
if (!Object.keys(metadataFields).length) {
|
|
130
|
+
errors.push(issue({
|
|
131
|
+
severity: 'error',
|
|
132
|
+
code: 'metadata_snapshot_missing',
|
|
133
|
+
message: `No metadata snapshot fields were found for ${locale}.`,
|
|
134
|
+
locale,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
if (!keywords.length) {
|
|
138
|
+
warnings.push(issue({
|
|
139
|
+
code: 'locale_keyword_context_missing',
|
|
140
|
+
message: `No ASO keyword rows were found for ${locale}.`,
|
|
141
|
+
locale,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
for (const [field, rawValue] of Object.entries(metadataFields).sort(([a], [b]) => a.localeCompare(b))) {
|
|
145
|
+
const value = rawValue === null || rawValue === undefined ? '' : String(rawValue);
|
|
146
|
+
const fieldWarnings = [];
|
|
147
|
+
const matches = [];
|
|
148
|
+
for (const keyword of keywords) {
|
|
149
|
+
const positions = matchKeywordInValue(value, keyword.keyword);
|
|
150
|
+
if (!positions.length)
|
|
151
|
+
continue;
|
|
152
|
+
matches.push({
|
|
153
|
+
...keyword,
|
|
154
|
+
matchCount: positions.length,
|
|
155
|
+
positions,
|
|
156
|
+
});
|
|
157
|
+
if (positions.length > 1) {
|
|
158
|
+
fieldWarnings.push(issue({
|
|
159
|
+
code: 'keyword_repeated_in_field',
|
|
160
|
+
message: `Keyword "${keyword.keyword}" appears ${positions.length} times in ${field}.`,
|
|
161
|
+
locale,
|
|
162
|
+
field,
|
|
163
|
+
keyword: keyword.keyword,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!fieldIndexedForAppleSearch(field) && matches.length) {
|
|
168
|
+
fieldWarnings.push(issue({
|
|
169
|
+
severity: 'info',
|
|
170
|
+
code: 'non_indexed_field_keyword_match',
|
|
171
|
+
message: `${field} contains matched keyword context but is not a primary Apple indexed metadata field.`,
|
|
172
|
+
locale,
|
|
173
|
+
field,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
fieldReports.push({
|
|
177
|
+
field,
|
|
178
|
+
value,
|
|
179
|
+
indexedForAppleSearch: fieldIndexedForAppleSearch(field),
|
|
180
|
+
notes: [indexedFieldNote(field)],
|
|
181
|
+
matches,
|
|
182
|
+
warnings: fieldWarnings,
|
|
183
|
+
});
|
|
184
|
+
warnings.push(...fieldWarnings.filter((warning) => warning.severity !== 'info'));
|
|
185
|
+
}
|
|
186
|
+
const coverage = keywords.map((keyword) => {
|
|
187
|
+
const fields = fieldReports
|
|
188
|
+
.map((field) => {
|
|
189
|
+
const match = field.matches.find((candidate) => candidate.normalizedKeyword === keyword.normalizedKeyword);
|
|
190
|
+
return match
|
|
191
|
+
? { field: field.field, matchCount: match.matchCount, positions: match.positions }
|
|
192
|
+
: null;
|
|
193
|
+
})
|
|
194
|
+
.filter((field) => Boolean(field));
|
|
195
|
+
const keywordWarnings = [];
|
|
196
|
+
if (!fields.length) {
|
|
197
|
+
keywordWarnings.push(issue({
|
|
198
|
+
code: 'keyword_not_detected',
|
|
199
|
+
message: `Keyword "${keyword.keyword}" is not detected in any metadata field for ${locale}.`,
|
|
200
|
+
locale,
|
|
201
|
+
keyword: keyword.keyword,
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
if (fields.length > 1) {
|
|
205
|
+
keywordWarnings.push(issue({
|
|
206
|
+
code: 'keyword_detected_in_multiple_fields',
|
|
207
|
+
message: `Keyword "${keyword.keyword}" appears in ${fields.length} metadata fields for ${locale}.`,
|
|
208
|
+
locale,
|
|
209
|
+
keyword: keyword.keyword,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
if (typeof keyword.difficulty === 'number' && keyword.difficulty >= 70) {
|
|
213
|
+
keywordWarnings.push(issue({
|
|
214
|
+
severity: 'info',
|
|
215
|
+
code: 'high_difficulty_keyword',
|
|
216
|
+
message: `Keyword "${keyword.keyword}" has high difficulty (${keyword.difficulty}).`,
|
|
217
|
+
locale,
|
|
218
|
+
keyword: keyword.keyword,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
if (typeof keyword.popularity === 'number' && keyword.popularity <= 20) {
|
|
222
|
+
keywordWarnings.push(issue({
|
|
223
|
+
severity: 'info',
|
|
224
|
+
code: 'low_popularity_keyword',
|
|
225
|
+
message: `Keyword "${keyword.keyword}" has low popularity (${keyword.popularity}).`,
|
|
226
|
+
locale,
|
|
227
|
+
keyword: keyword.keyword,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
warnings.push(...keywordWarnings.filter((warning) => warning.severity !== 'info'));
|
|
231
|
+
return {
|
|
232
|
+
...keyword,
|
|
233
|
+
detected: fields.length > 0,
|
|
234
|
+
fields,
|
|
235
|
+
warnings: keywordWarnings,
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
locale,
|
|
240
|
+
fields: fieldReports,
|
|
241
|
+
keywordCoverage: coverage,
|
|
242
|
+
unassignedKeywords: coverage.filter((keyword) => !keyword.detected),
|
|
243
|
+
warnings,
|
|
244
|
+
errors,
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
const allWarnings = [
|
|
248
|
+
...globalWarnings,
|
|
249
|
+
...localeReports.flatMap((locale) => locale.warnings),
|
|
250
|
+
];
|
|
251
|
+
const allErrors = [
|
|
252
|
+
...globalErrors,
|
|
253
|
+
...localeReports.flatMap((locale) => locale.errors),
|
|
254
|
+
];
|
|
255
|
+
const fieldCount = localeReports.reduce((sum, locale) => sum + locale.fields.length, 0);
|
|
256
|
+
const keywordKeys = new Set(localeReports.flatMap((locale) => locale.keywordCoverage.map((keyword) => `${locale.locale}|${keyword.normalizedKeyword}`)));
|
|
257
|
+
const detectedKeywordCount = localeReports.reduce((sum, locale) => sum + locale.keywordCoverage.filter((keyword) => keyword.detected).length, 0);
|
|
258
|
+
const unassignedKeywordCount = localeReports.reduce((sum, locale) => sum + locale.unassignedKeywords.length, 0);
|
|
259
|
+
return {
|
|
260
|
+
kind: 'localizeaso_aso_keyword_detection_report',
|
|
261
|
+
version: 1,
|
|
262
|
+
surface: input.surface,
|
|
263
|
+
generatedAt: input.generatedAt ?? new Date().toISOString(),
|
|
264
|
+
agentCompatibility: {
|
|
265
|
+
audience: 'any_coding_agent',
|
|
266
|
+
protocol: 'provider_neutral_json',
|
|
267
|
+
requiresHumanApprovalBeforeApply: true,
|
|
268
|
+
notes: [
|
|
269
|
+
'Any coding agent can consume this report; it is not tied to a specific agent vendor or hosted LocalizeASO model.',
|
|
270
|
+
'Use keywordCoverage, fields.matches, unassignedKeywords, warnings, and errors as proposal context only.',
|
|
271
|
+
'The report is read-only and never grants approval, apply, publish, or App Store Connect submit permission.',
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
agents: input.agents ?? ['any-coding-agent'],
|
|
275
|
+
source: {
|
|
276
|
+
metadataSnapshotKeys: input.metadataSnapshotKeys ?? [],
|
|
277
|
+
keywordContextSources: keywordContext.sources,
|
|
278
|
+
},
|
|
279
|
+
summary: {
|
|
280
|
+
localeCount: localeReports.length,
|
|
281
|
+
fieldCount,
|
|
282
|
+
keywordCount: keywordKeys.size,
|
|
283
|
+
detectedKeywordCount,
|
|
284
|
+
unassignedKeywordCount,
|
|
285
|
+
warningCount: allWarnings.length,
|
|
286
|
+
errorCount: allErrors.length,
|
|
287
|
+
},
|
|
288
|
+
locales: localeReports,
|
|
289
|
+
warnings: allWarnings,
|
|
290
|
+
errors: allErrors,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const APP_STORE_LOCALES: {
|
|
2
|
+
code: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}[];
|
|
5
|
+
export declare const LOCALE_TO_REGION_CODE: Record<string, string>;
|
|
6
|
+
export declare const ASC_LOCALE_ALIASES: Record<string, string>;
|
|
7
|
+
export declare const FULL_EDIT_MODE: readonly ["PREPARE_FOR_SUBMISSION", "METADATA_REJECTED", "WAITING_FOR_EXPORT_COMPLIANCE", "REJECTED", "DEVELOPER_REJECTED"];
|
|
8
|
+
export declare const CAN_CREATE_NEW_VERSION: readonly ["READY_FOR_DISTRIBUTION"];
|
|
9
|
+
export declare const APP_TITLE_STYLE_VALUES: readonly ["appname_colon_main_keyword", "main_keyword_colon_appname", "main_keyword_dash_appname", "appname_dash_main_keyword", "main_keywords_only"];
|
|
10
|
+
export type AppTitleStyle = (typeof APP_TITLE_STYLE_VALUES)[number];
|
|
11
|
+
export declare const DEFAULT_APP_TITLE_STYLE: AppTitleStyle;
|
|
12
|
+
export declare const APP_TITLE_CASING_VALUES: readonly ["language_optimized", "capitalize_each_word", "all_lowercase"];
|
|
13
|
+
export type AppTitleCasing = (typeof APP_TITLE_CASING_VALUES)[number];
|
|
14
|
+
export declare const DEFAULT_APP_TITLE_CASING: AppTitleCasing;
|
|
15
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB;;;GAwC7B,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAwCxD,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAyBrD,CAAC;AAEF,eAAO,MAAM,cAAc,6HAMjB,CAAC;AAEX,eAAO,MAAM,sBAAsB,qCAAsC,CAAC;AAE1E,eAAO,MAAM,sBAAsB,uJAMzB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,eAAO,MAAM,uBAAuB,EAAE,aAA4C,CAAC;AAEnF,eAAO,MAAM,uBAAuB,0EAI1B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,eAAO,MAAM,wBAAwB,EAAE,cAAqC,CAAC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export const APP_STORE_LOCALES = [
|
|
2
|
+
{ code: 'ar-SA', name: 'Arabic' },
|
|
3
|
+
{ code: 'ca', name: 'Catalan' },
|
|
4
|
+
{ code: 'cs', name: 'Czech' },
|
|
5
|
+
{ code: 'da', name: 'Danish' },
|
|
6
|
+
{ code: 'de-DE', name: 'German' },
|
|
7
|
+
{ code: 'el', name: 'Greek' },
|
|
8
|
+
{ code: 'en-AU', name: 'English (Australia)' },
|
|
9
|
+
{ code: 'en-CA', name: 'English (Canada)' },
|
|
10
|
+
{ code: 'en-GB', name: 'English (UK)' },
|
|
11
|
+
{ code: 'en-US', name: 'English (US)' },
|
|
12
|
+
{ code: 'es-ES', name: 'Spanish (Spain)' },
|
|
13
|
+
{ code: 'es-MX', name: 'Spanish (Mexico)' },
|
|
14
|
+
{ code: 'fi', name: 'Finnish' },
|
|
15
|
+
{ code: 'fr-CA', name: 'French (Canada)' },
|
|
16
|
+
{ code: 'fr-FR', name: 'French (France)' },
|
|
17
|
+
{ code: 'he', name: 'Hebrew' },
|
|
18
|
+
{ code: 'hi', name: 'Hindi' },
|
|
19
|
+
{ code: 'hr', name: 'Croatian' },
|
|
20
|
+
{ code: 'hu', name: 'Hungarian' },
|
|
21
|
+
{ code: 'id', name: 'Indonesian' },
|
|
22
|
+
{ code: 'it', name: 'Italian' },
|
|
23
|
+
{ code: 'ja', name: 'Japanese' },
|
|
24
|
+
{ code: 'ko', name: 'Korean' },
|
|
25
|
+
{ code: 'ms', name: 'Malay' },
|
|
26
|
+
{ code: 'nl-NL', name: 'Dutch' },
|
|
27
|
+
{ code: 'no', name: 'Norwegian' },
|
|
28
|
+
{ code: 'pl', name: 'Polish' },
|
|
29
|
+
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
|
30
|
+
{ code: 'pt-PT', name: 'Portuguese (Portugal)' },
|
|
31
|
+
{ code: 'ro', name: 'Romanian' },
|
|
32
|
+
{ code: 'ru', name: 'Russian' },
|
|
33
|
+
{ code: 'sk', name: 'Slovak' },
|
|
34
|
+
{ code: 'sv', name: 'Swedish' },
|
|
35
|
+
{ code: 'th', name: 'Thai' },
|
|
36
|
+
{ code: 'tr', name: 'Turkish' },
|
|
37
|
+
{ code: 'uk', name: 'Ukrainian' },
|
|
38
|
+
{ code: 'vi', name: 'Vietnamese' },
|
|
39
|
+
{ code: 'zh-Hans', name: 'Chinese (Simplified)' },
|
|
40
|
+
{ code: 'zh-Hant', name: 'Chinese (Traditional)' },
|
|
41
|
+
];
|
|
42
|
+
export const LOCALE_TO_REGION_CODE = {
|
|
43
|
+
'ar-SA': 'SA',
|
|
44
|
+
ca: 'ES',
|
|
45
|
+
cs: 'CZ',
|
|
46
|
+
da: 'DK',
|
|
47
|
+
'de-DE': 'DE',
|
|
48
|
+
el: 'GR',
|
|
49
|
+
'en-AU': 'AU',
|
|
50
|
+
'en-CA': 'CA',
|
|
51
|
+
'en-GB': 'GB',
|
|
52
|
+
'en-US': 'US',
|
|
53
|
+
'es-ES': 'ES',
|
|
54
|
+
'es-MX': 'MX',
|
|
55
|
+
fi: 'FI',
|
|
56
|
+
'fr-CA': 'CA',
|
|
57
|
+
'fr-FR': 'FR',
|
|
58
|
+
he: 'IL',
|
|
59
|
+
hi: 'IN',
|
|
60
|
+
hr: 'HR',
|
|
61
|
+
hu: 'HU',
|
|
62
|
+
id: 'ID',
|
|
63
|
+
it: 'IT',
|
|
64
|
+
ja: 'JP',
|
|
65
|
+
ko: 'KR',
|
|
66
|
+
ms: 'MY',
|
|
67
|
+
'nl-NL': 'NL',
|
|
68
|
+
no: 'NO',
|
|
69
|
+
pl: 'PL',
|
|
70
|
+
'pt-BR': 'BR',
|
|
71
|
+
'pt-PT': 'PT',
|
|
72
|
+
ro: 'RO',
|
|
73
|
+
ru: 'RU',
|
|
74
|
+
sk: 'SK',
|
|
75
|
+
sv: 'SE',
|
|
76
|
+
th: 'TH',
|
|
77
|
+
tr: 'TR',
|
|
78
|
+
uk: 'UA',
|
|
79
|
+
vi: 'VN',
|
|
80
|
+
'zh-Hans': 'CN',
|
|
81
|
+
'zh-Hant': 'TW',
|
|
82
|
+
};
|
|
83
|
+
export const ASC_LOCALE_ALIASES = {
|
|
84
|
+
'ca-ES': 'ca',
|
|
85
|
+
'cs-CZ': 'cs',
|
|
86
|
+
'da-DK': 'da',
|
|
87
|
+
'el-GR': 'el',
|
|
88
|
+
'fi-FI': 'fi',
|
|
89
|
+
'he-IL': 'he',
|
|
90
|
+
'hi-IN': 'hi',
|
|
91
|
+
'hr-HR': 'hr',
|
|
92
|
+
'hu-HU': 'hu',
|
|
93
|
+
'id-ID': 'id',
|
|
94
|
+
'it-IT': 'it',
|
|
95
|
+
'ja-JP': 'ja',
|
|
96
|
+
'ko-KR': 'ko',
|
|
97
|
+
'ms-MY': 'ms',
|
|
98
|
+
'no-NO': 'no',
|
|
99
|
+
'pl-PL': 'pl',
|
|
100
|
+
'ro-RO': 'ro',
|
|
101
|
+
'ru-RU': 'ru',
|
|
102
|
+
'sk-SK': 'sk',
|
|
103
|
+
'sv-SE': 'sv',
|
|
104
|
+
'th-TH': 'th',
|
|
105
|
+
'tr-TR': 'tr',
|
|
106
|
+
'uk-UA': 'uk',
|
|
107
|
+
'vi-VN': 'vi',
|
|
108
|
+
};
|
|
109
|
+
export const FULL_EDIT_MODE = [
|
|
110
|
+
'PREPARE_FOR_SUBMISSION',
|
|
111
|
+
'METADATA_REJECTED',
|
|
112
|
+
'WAITING_FOR_EXPORT_COMPLIANCE',
|
|
113
|
+
'REJECTED',
|
|
114
|
+
'DEVELOPER_REJECTED',
|
|
115
|
+
];
|
|
116
|
+
export const CAN_CREATE_NEW_VERSION = ['READY_FOR_DISTRIBUTION'];
|
|
117
|
+
export const APP_TITLE_STYLE_VALUES = [
|
|
118
|
+
'appname_colon_main_keyword',
|
|
119
|
+
'main_keyword_colon_appname',
|
|
120
|
+
'main_keyword_dash_appname',
|
|
121
|
+
'appname_dash_main_keyword',
|
|
122
|
+
'main_keywords_only',
|
|
123
|
+
];
|
|
124
|
+
export const DEFAULT_APP_TITLE_STYLE = 'main_keyword_colon_appname';
|
|
125
|
+
export const APP_TITLE_CASING_VALUES = [
|
|
126
|
+
'language_optimized',
|
|
127
|
+
'capitalize_each_word',
|
|
128
|
+
'all_lowercase',
|
|
129
|
+
];
|
|
130
|
+
export const DEFAULT_APP_TITLE_CASING = 'language_optimized';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type CrossLocalizationTier = 'primary_exact' | 'primary_language' | 'target_exact' | 'target_language' | 'cross_exact' | 'cross_language';
|
|
2
|
+
export declare const CROSS_LOCALIZATION_TIER_WEIGHT: Record<CrossLocalizationTier, number>;
|
|
3
|
+
export declare function resolveCrossLocalizedLocales(args: {
|
|
4
|
+
targetLocale: string;
|
|
5
|
+
primaryLocale?: string | null;
|
|
6
|
+
}): {
|
|
7
|
+
primaryLocale: string;
|
|
8
|
+
targetLocale: string;
|
|
9
|
+
crossExactLocales: Set<string>;
|
|
10
|
+
crossLanguageCodes: Set<string>;
|
|
11
|
+
};
|
|
12
|
+
export declare function resolveCrossLocalizationTier(args: {
|
|
13
|
+
keywordLocale: string;
|
|
14
|
+
targetLocale: string;
|
|
15
|
+
primaryLocale?: string | null;
|
|
16
|
+
includeCrossLocalization?: boolean;
|
|
17
|
+
}): CrossLocalizationTier | null;
|
|
18
|
+
export declare function selectCrossLocalizedRows<T extends {
|
|
19
|
+
locale: string;
|
|
20
|
+
}>(rows: T[] | undefined, args: {
|
|
21
|
+
targetLocale: string;
|
|
22
|
+
primaryLocale?: string | null;
|
|
23
|
+
includeCrossLocalization?: boolean;
|
|
24
|
+
}): {
|
|
25
|
+
row: T;
|
|
26
|
+
tier: CrossLocalizationTier;
|
|
27
|
+
weight: number;
|
|
28
|
+
}[];
|
|
29
|
+
//# sourceMappingURL=cross-localization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cross-localization.d.ts","sourceRoot":"","sources":["../src/cross-localization.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,qBAAqB,GAC7B,eAAe,GACf,kBAAkB,GAClB,cAAc,GACd,iBAAiB,GACjB,aAAa,GACb,gBAAgB,CAAC;AAErB,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAOhF,CAAC;AA6GF,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;;;;;EA+BA;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC,GAAG,qBAAqB,GAAG,IAAI,CAoC/B;AAED,wBAAgB,wBAAwB,CAAC,CAAC,SAAS;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACnE,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,EACrB,IAAI,EAAE;IACJ,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;SAE4C,CAAC;UAAQ,qBAAqB;YAAU,MAAM;IAoB5F"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { ASC_LOCALES, normalizeAscLocale } from './locales.js';
|
|
2
|
+
export const CROSS_LOCALIZATION_TIER_WEIGHT = {
|
|
3
|
+
primary_exact: 1200,
|
|
4
|
+
primary_language: 1050,
|
|
5
|
+
target_exact: 900,
|
|
6
|
+
target_language: 760,
|
|
7
|
+
cross_exact: 560,
|
|
8
|
+
cross_language: 420,
|
|
9
|
+
};
|
|
10
|
+
// Cross-localization locale graph derived from the ASO reference matrix:
|
|
11
|
+
// https://aso.dev/metadata/cross-localization/
|
|
12
|
+
// The graph encodes locale-to-locale indexing relationships (default/additional).
|
|
13
|
+
const RAW_CROSS_LOCALE_GRAPH = {
|
|
14
|
+
'ar-SA': ['en-GB', 'en-US', 'fr-FR', 'zh-Hans', 'zh-Hant', 'vi'],
|
|
15
|
+
ca: ['es-ES', 'en-GB'],
|
|
16
|
+
cs: ['en-GB'],
|
|
17
|
+
da: ['en-GB'],
|
|
18
|
+
'de-DE': ['en-GB', 'fr-FR', 'it'],
|
|
19
|
+
el: ['en-GB'],
|
|
20
|
+
'en-AU': ['ar-SA', 'zh-Hans', 'zh-Hant', 'vi', 'en-GB'],
|
|
21
|
+
'en-CA': ['fr-CA', 'en-US', 'en-GB'],
|
|
22
|
+
'en-GB': ASC_LOCALES.filter((locale) => locale !== 'en-GB'),
|
|
23
|
+
'en-US': ['ar-SA', 'zh-Hans', 'zh-Hant', 'fr-FR', 'ko', 'pt-BR', 'ru', 'es-MX', 'vi', 'ja'],
|
|
24
|
+
'es-ES': ['ca', 'en-GB'],
|
|
25
|
+
'es-MX': ['en-GB', 'en-US', 'pt-BR', 'fr-FR'],
|
|
26
|
+
fi: ['en-GB'],
|
|
27
|
+
'fr-CA': ['en-CA', 'en-US', 'en-GB'],
|
|
28
|
+
'fr-FR': [
|
|
29
|
+
'en-GB',
|
|
30
|
+
'ar-SA',
|
|
31
|
+
'de-DE',
|
|
32
|
+
'it',
|
|
33
|
+
'ja',
|
|
34
|
+
'ko',
|
|
35
|
+
'pt-BR',
|
|
36
|
+
'ru',
|
|
37
|
+
'zh-Hans',
|
|
38
|
+
'es-MX',
|
|
39
|
+
'zh-Hant',
|
|
40
|
+
'vi',
|
|
41
|
+
'nl-NL',
|
|
42
|
+
],
|
|
43
|
+
he: ['en-GB'],
|
|
44
|
+
hi: ['en-GB'],
|
|
45
|
+
hr: ['en-GB'],
|
|
46
|
+
hu: ['en-GB'],
|
|
47
|
+
id: ['en-GB'],
|
|
48
|
+
it: ['en-GB', 'de-DE', 'fr-FR'],
|
|
49
|
+
ja: ['en-US', 'en-GB', 'fr-FR'],
|
|
50
|
+
ko: ['en-US', 'en-GB', 'fr-FR'],
|
|
51
|
+
ms: ['en-GB'],
|
|
52
|
+
'nl-NL': ['en-GB', 'fr-FR'],
|
|
53
|
+
no: ['en-GB'],
|
|
54
|
+
pl: ['en-GB'],
|
|
55
|
+
'pt-BR': ['en-US', 'en-GB', 'es-MX', 'fr-FR'],
|
|
56
|
+
'pt-PT': ['en-GB'],
|
|
57
|
+
ro: ['en-GB'],
|
|
58
|
+
ru: ['en-US', 'en-GB', 'fr-FR'],
|
|
59
|
+
sk: ['en-GB'],
|
|
60
|
+
sv: ['en-GB'],
|
|
61
|
+
th: ['en-GB'],
|
|
62
|
+
tr: ['en-GB'],
|
|
63
|
+
uk: ['en-GB'],
|
|
64
|
+
vi: ['en-US', 'en-AU', 'en-GB', 'fr-FR'],
|
|
65
|
+
'zh-Hans': ['en-US', 'en-GB', 'fr-FR', 'zh-Hant'],
|
|
66
|
+
'zh-Hant': ['en-US', 'en-GB', 'fr-FR', 'zh-Hans'],
|
|
67
|
+
};
|
|
68
|
+
const ASC_LOCALE_SET = new Set(ASC_LOCALES.map((locale) => normalizeAscLocale(locale)));
|
|
69
|
+
function localeLanguage(locale) {
|
|
70
|
+
return normalizeAscLocale(locale).split('-')[0] ?? '';
|
|
71
|
+
}
|
|
72
|
+
function isLanguageOnly(locale) {
|
|
73
|
+
const normalized = normalizeAscLocale(locale);
|
|
74
|
+
return normalized.length > 0 && !normalized.includes('-');
|
|
75
|
+
}
|
|
76
|
+
function normalizeGraph(graph) {
|
|
77
|
+
const normalized = {};
|
|
78
|
+
for (const locale of ASC_LOCALES) {
|
|
79
|
+
const normalizedLocale = normalizeAscLocale(locale);
|
|
80
|
+
normalized[normalizedLocale] = new Set();
|
|
81
|
+
}
|
|
82
|
+
for (const [source, targets] of Object.entries(graph)) {
|
|
83
|
+
const sourceLocale = normalizeAscLocale(source);
|
|
84
|
+
if (!sourceLocale || !ASC_LOCALE_SET.has(sourceLocale))
|
|
85
|
+
continue;
|
|
86
|
+
for (const rawTarget of targets) {
|
|
87
|
+
const targetLocale = normalizeAscLocale(rawTarget);
|
|
88
|
+
if (!targetLocale || !ASC_LOCALE_SET.has(targetLocale))
|
|
89
|
+
continue;
|
|
90
|
+
normalized[sourceLocale]?.add(targetLocale);
|
|
91
|
+
normalized[targetLocale]?.add(sourceLocale);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return Object.fromEntries(Object.entries(normalized).map(([locale, connected]) => [locale, Array.from(connected)]));
|
|
95
|
+
}
|
|
96
|
+
const CROSS_LOCALE_GRAPH = normalizeGraph(RAW_CROSS_LOCALE_GRAPH);
|
|
97
|
+
function getSameLanguageLocales(locale) {
|
|
98
|
+
const language = localeLanguage(locale);
|
|
99
|
+
if (!language)
|
|
100
|
+
return new Set();
|
|
101
|
+
return new Set(ASC_LOCALES.map((entry) => normalizeAscLocale(entry)).filter((entry) => localeLanguage(entry) === language));
|
|
102
|
+
}
|
|
103
|
+
export function resolveCrossLocalizedLocales(args) {
|
|
104
|
+
const targetLocale = normalizeAscLocale(args.targetLocale);
|
|
105
|
+
if (!targetLocale) {
|
|
106
|
+
return {
|
|
107
|
+
primaryLocale: '',
|
|
108
|
+
targetLocale: '',
|
|
109
|
+
crossExactLocales: new Set(),
|
|
110
|
+
crossLanguageCodes: new Set(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const primaryLocale = normalizeAscLocale(args.primaryLocale ?? '');
|
|
114
|
+
const crossExactLocales = new Set();
|
|
115
|
+
for (const candidate of CROSS_LOCALE_GRAPH[targetLocale] ?? []) {
|
|
116
|
+
crossExactLocales.add(candidate);
|
|
117
|
+
}
|
|
118
|
+
for (const candidate of getSameLanguageLocales(targetLocale)) {
|
|
119
|
+
if (candidate !== targetLocale)
|
|
120
|
+
crossExactLocales.add(candidate);
|
|
121
|
+
}
|
|
122
|
+
if (primaryLocale && primaryLocale !== targetLocale) {
|
|
123
|
+
crossExactLocales.add(primaryLocale);
|
|
124
|
+
}
|
|
125
|
+
const crossLanguageCodes = new Set();
|
|
126
|
+
for (const locale of crossExactLocales) {
|
|
127
|
+
const language = localeLanguage(locale);
|
|
128
|
+
if (language)
|
|
129
|
+
crossLanguageCodes.add(language);
|
|
130
|
+
}
|
|
131
|
+
return { primaryLocale, targetLocale, crossExactLocales, crossLanguageCodes };
|
|
132
|
+
}
|
|
133
|
+
export function resolveCrossLocalizationTier(args) {
|
|
134
|
+
const keywordLocale = normalizeAscLocale(args.keywordLocale);
|
|
135
|
+
const includeCrossLocalization = args.includeCrossLocalization !== false;
|
|
136
|
+
const { primaryLocale, targetLocale, crossExactLocales, crossLanguageCodes } = resolveCrossLocalizedLocales({
|
|
137
|
+
targetLocale: args.targetLocale,
|
|
138
|
+
primaryLocale: args.primaryLocale,
|
|
139
|
+
});
|
|
140
|
+
if (!keywordLocale || !targetLocale)
|
|
141
|
+
return null;
|
|
142
|
+
if (keywordLocale === targetLocale)
|
|
143
|
+
return 'target_exact';
|
|
144
|
+
const keywordLanguage = localeLanguage(keywordLocale);
|
|
145
|
+
if (!keywordLanguage)
|
|
146
|
+
return null;
|
|
147
|
+
if (!includeCrossLocalization) {
|
|
148
|
+
if (isLanguageOnly(keywordLocale) && keywordLanguage === localeLanguage(targetLocale)) {
|
|
149
|
+
return 'target_language';
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
if (primaryLocale && keywordLocale === primaryLocale)
|
|
154
|
+
return 'primary_exact';
|
|
155
|
+
if (primaryLocale && isLanguageOnly(keywordLocale) && keywordLanguage === localeLanguage(primaryLocale)) {
|
|
156
|
+
return 'primary_language';
|
|
157
|
+
}
|
|
158
|
+
if (isLanguageOnly(keywordLocale) && keywordLanguage === localeLanguage(targetLocale)) {
|
|
159
|
+
return 'target_language';
|
|
160
|
+
}
|
|
161
|
+
if (crossExactLocales.has(keywordLocale))
|
|
162
|
+
return 'cross_exact';
|
|
163
|
+
if (isLanguageOnly(keywordLocale) && crossLanguageCodes.has(keywordLanguage)) {
|
|
164
|
+
return 'cross_language';
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
export function selectCrossLocalizedRows(rows, args) {
|
|
169
|
+
if (!rows?.length)
|
|
170
|
+
return [];
|
|
171
|
+
const selected = [];
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
const tier = resolveCrossLocalizationTier({
|
|
174
|
+
keywordLocale: row.locale,
|
|
175
|
+
targetLocale: args.targetLocale,
|
|
176
|
+
primaryLocale: args.primaryLocale,
|
|
177
|
+
includeCrossLocalization: args.includeCrossLocalization,
|
|
178
|
+
});
|
|
179
|
+
if (!tier)
|
|
180
|
+
continue;
|
|
181
|
+
selected.push({
|
|
182
|
+
row,
|
|
183
|
+
tier,
|
|
184
|
+
weight: CROSS_LOCALIZATION_TIER_WEIGHT[tier],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
selected.sort((left, right) => right.weight - left.weight);
|
|
188
|
+
return selected;
|
|
189
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type KeywordField = "title" | "subtitle" | "keywords";
|
|
2
|
+
export type DuplicateEntry = {
|
|
3
|
+
keyword: string;
|
|
4
|
+
fields: KeywordField[];
|
|
5
|
+
};
|
|
6
|
+
export declare const DEFAULT_IGNORED_DUPLICATE_KEYWORDS: readonly ["a", "an", "and", "or", "the", "to", "for", "of", "in", "on", "with", "der", "die", "das", "und", "mit", "fur", "fuer", "de", "la", "le", "les", "et", "el", "los", "las", "y", "en", "para", "con", "por", "il", "lo", "gli", "e", "di", "per", "com"];
|
|
7
|
+
export declare function normalizeKeywordToken(value: string): string;
|
|
8
|
+
export declare function tokenizeText(value: string): string[];
|
|
9
|
+
export declare function tokenizeKeywordField(value: string): string[];
|
|
10
|
+
export declare function findDuplicateKeywords(fields: {
|
|
11
|
+
title?: string;
|
|
12
|
+
subtitle?: string;
|
|
13
|
+
keywords?: string;
|
|
14
|
+
}, options?: {
|
|
15
|
+
ignoreKeywords?: Iterable<string>;
|
|
16
|
+
}): DuplicateEntry[];
|
|
17
|
+
//# sourceMappingURL=dedupe.d.ts.map
|