├── resources
├── iso-639.csv.gz
├── iso-639.sql.gz
└── wikipedia.csv
├── .gitignore
├── phpunit.xml
├── composer.json
├── .github
└── workflows
│ ├── test.yml
│ ├── lint.yml
│ └── release.yml
├── .php-cs-fixer.php
├── README.md
├── src
└── ISO639.php
└── tests
└── ISO639Test.php
/resources/iso-639.csv.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matriphe/php-iso-639/HEAD/resources/iso-639.csv.gz
--------------------------------------------------------------------------------
/resources/iso-639.sql.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matriphe/php-iso-639/HEAD/resources/iso-639.sql.gz
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.lock
3 | *.cache
4 |
5 | # PhpStorm IDE
6 | /.idea
7 |
8 | # Coverage reports
9 | /coverage
10 | coverage.xml
11 | .phpunit.cache
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | tests
8 |
9 |
10 |
11 |
12 | src
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "matriphe/iso-639",
3 | "description": "PHP library to convert ISO-639-1 code to language name.",
4 | "keywords": ["iso", "iso-639", "639", "lang", "language", "laravel"],
5 | "type": "library",
6 | "require-dev": {
7 | "phpunit/phpunit": ">=10",
8 | "friendsofphp/php-cs-fixer": "^3.75"
9 | },
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Muhammad Zamroni",
14 | "email": "halo@matriphe.com"
15 | }
16 | ],
17 | "require": {
18 | "php": "8.*"
19 | },
20 | "suggest": {
21 | "ext-mbstring": "For better multibyte character support in text transformations",
22 | "ext-xdebug": "For code coverage when running tests"
23 | },
24 | "autoload": {
25 | "psr-4": {
26 | "Matriphe\\ISO639\\": "src/"
27 | }
28 | },
29 | "scripts": {
30 | "lint": "php-cs-fixer fix --dry-run --diff",
31 | "lint:fix": "php-cs-fixer fix",
32 | "test": "phpunit",
33 | "test:coverage": "XDEBUG_MODE=coverage phpunit --coverage-text",
34 | "test:coverage-html": "XDEBUG_MODE=coverage phpunit --coverage-html coverage",
35 | "test:coverage-xml": "XDEBUG_MODE=coverage phpunit --coverage-clover coverage.xml",
36 | "test:no-coverage": "phpunit --no-coverage",
37 | "check": [
38 | "@lint",
39 | "@test"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | run-test:
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | operating-system: [ ubuntu-latest ]
20 | php-version:
21 | - "8.4"
22 | - "8.3"
23 | - "8.2"
24 | - "8.1"
25 |
26 | name: PHP ${{ matrix.php-version }}
27 | runs-on: ${{ matrix.operating-system }}
28 |
29 | steps:
30 | - name: Checkout code
31 | uses: actions/checkout@v5
32 |
33 | - name: Setup PHP
34 | uses: shivammathur/setup-php@v2
35 | with:
36 | php-version: ${{ matrix.php-version }}
37 | extensions: mbstring xdebug
38 | tools: composer:v2
39 |
40 | - name: Check PHP extensions
41 | run: php -m
42 |
43 | - name: Validate composer.json and composer.lock
44 | run: composer validate --strict
45 |
46 | - name: Cache Composer packages
47 | id: composer-cache
48 | uses: actions/cache@v3
49 | with:
50 | path: vendor
51 | key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
52 | restore-keys: |
53 | ${{ runner.os }}-php-${{ matrix.php-version }}
54 |
55 | - name: Install dependencies
56 | run: composer install --prefer-dist --no-progress
57 |
58 | - name: Run test
59 | run: composer run-script test:coverage
60 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Code Linting
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | lint:
10 | name: Lint
11 | runs-on: ubuntu-latest
12 |
13 | permissions:
14 | contents: read
15 | packages: read
16 | # To report GitHub Actions status checks
17 | statuses: write
18 |
19 | steps:
20 | - name: Checkout code
21 | uses: actions/checkout@v5
22 | with:
23 | # super-linter needs the full git history to get the
24 | # list of files that changed across commits
25 | fetch-depth: 0
26 | persist-credentials: false
27 |
28 | - name: Setup PHP
29 | uses: shivammathur/setup-php@v2
30 | with:
31 | php-version: 8.4
32 | extensions: mbstring
33 | tools: composer:v2
34 |
35 | - name: Cache Composer packages
36 | id: composer-cache
37 | uses: actions/cache@v3
38 | with:
39 | path: vendor
40 | key: ${{ runner.os }}-php-8.4-${{ hashFiles('**/composer.lock') }}
41 | restore-keys: |
42 | ${{ runner.os }}-php-8.4
43 |
44 | - name: Install dependencies
45 | run: composer install --prefer-dist --no-progress
46 |
47 | - name: Run PHP-CS-Fixer
48 | run: composer run-script lint
49 |
50 | - name: Check for changes
51 | run: |
52 | if [ -n "$(git status --porcelain)" ]; then
53 | echo "Code style issues found. Please run 'composer run-script fix' locally."
54 | git diff
55 | exit 1
56 | else
57 | echo "No code style issues found."
58 | fi
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | in([
5 | __DIR__ . '/src',
6 | __DIR__ . '/tests',
7 | ])
8 | ->exclude('vendor');
9 |
10 | return (new PhpCsFixer\Config())
11 | ->setRiskyAllowed(true)
12 | ->setRules([
13 | '@PSR12' => true,
14 | 'array_syntax' => ['syntax' => 'short'],
15 | 'binary_operator_spaces' => true,
16 | 'blank_line_after_opening_tag' => true,
17 | 'cast_spaces' => true,
18 | 'concat_space' => ['spacing' => 'one'],
19 | 'function_typehint_space' => true,
20 | 'lowercase_cast' => true,
21 | 'method_argument_space' => true,
22 | 'native_function_casing' => true,
23 | 'new_with_braces' => true,
24 | 'no_blank_lines_after_class_opening' => true,
25 | 'no_blank_lines_after_phpdoc' => true,
26 | 'no_closing_tag' => true,
27 | 'no_empty_phpdoc' => true,
28 | 'no_empty_statement' => true,
29 | 'no_extra_blank_lines' => [
30 | 'tokens' => [
31 | 'extra',
32 | 'throw',
33 | 'use',
34 | ]
35 | ],
36 | 'no_leading_import_slash' => true,
37 | 'no_leading_namespace_whitespace' => true,
38 | 'no_mixed_echo_print' => true,
39 | 'no_multiline_whitespace_around_double_arrow' => true,
40 | 'no_short_bool_cast' => true,
41 | 'no_singleline_whitespace_before_semicolons' => true,
42 | 'no_spaces_after_function_name' => true,
43 | 'no_spaces_around_offset' => true,
44 | 'no_spaces_inside_parenthesis' => true,
45 | 'no_trailing_comma_in_list_call' => true,
46 | 'no_trailing_whitespace' => true,
47 | 'no_trailing_whitespace_in_comment' => true,
48 | 'no_unused_imports' => true,
49 | 'no_whitespace_before_comma_in_array' => true,
50 | 'no_whitespace_in_blank_line' => true,
51 | 'normalize_index_brace' => true,
52 | 'object_operator_without_whitespace' => true,
53 | 'ordered_imports' => true,
54 | 'return_type_declaration' => true,
55 | 'short_scalar_cast' => true,
56 | 'single_blank_line_at_eof' => true,
57 | 'single_class_element_per_statement' => true,
58 | 'single_import_per_statement' => true,
59 | 'single_line_after_imports' => true,
60 | 'single_quote' => true,
61 | 'space_after_semicolon' => true,
62 | 'standardize_not_equals' => true,
63 | 'ternary_operator_spaces' => true,
64 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']],
65 | 'trim_array_spaces' => true,
66 | 'unary_operator_spaces' => true,
67 | 'visibility_required' => true,
68 | 'whitespace_after_comma_in_array' => true,
69 | ])
70 | ->setFinder($finder);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PHP ISO-639
2 |
3 | [](https://github.com/matriphe/php-iso-639/actions/workflows/test.yml)
4 | [](https://packagist.org/packages/matriphe/iso-639)
5 | [](https://packagist.org/packages/matriphe/iso-639)
6 |
7 | PHP library to convert ISO-639-1 code to language name, based on Wikipedia's [List of ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
8 |
9 | ## Installation
10 |
11 | For PHP 8.1 or latest:
12 |
13 | ```shell
14 | composer require matriphe/iso-639
15 | ```
16 |
17 | For older PHP version:
18 |
19 | ```shell
20 | composer require matriphe/iso-639:1.3
21 | ```
22 |
23 | ## Requirements
24 |
25 | - PHP 8.0 or higher
26 | - **Optional**: `ext-mbstring` extension for better multibyte character support
27 |
28 | The library will work without the `mbstring` extension, but for optimal handling of multibyte characters (like accented characters, Cyrillic, Arabic, etc.) in language names, it's recommended to install the `mbstring` extension.
29 |
30 | ## Usage Example
31 |
32 | ```php
33 | languageByCode1('en'); // English
42 | echo $iso->languageByCode1('id'); // Indonesian
43 | echo $iso->languageByCode1('jv'); // Javanese
44 |
45 | // Get native language name from ISO-639-1 code
46 | echo $iso->nativeByCode1('en'); // English
47 | echo $iso->nativeByCode1('id'); // Bahasa Indonesia
48 | echo $iso->nativeByCode1('jv'); // basa Jawa
49 |
50 | // Get native language name from ISO-639-1 code with capitalized
51 | echo $iso->nativeByCode1('en', true); // English
52 | echo $iso->nativeByCode1('id', true); // Bahasa Indonesia
53 | echo $iso->nativeByCode1('jv', true); // Basa Jawa
54 | echo $iso->nativeByCode1('hi', true); // हिन्दी, हिंदी
55 | echo $iso->nativeByCode1('th', true); // ไทย
56 | echo $iso->nativeByCode1('ko', true); // 한국어
57 | echo $iso->nativeByCode1('ja', true); // 日本語 (にほんご)
58 | echo $iso->nativeByCode1('zh', true); // '中文 (Zhōngwén), 汉语, 漢語
59 | echo $iso->nativeByCode1('ru', true); // Русский
60 | echo $iso->nativeByCode1('ar', true); // االعربية
61 | echo $iso->nativeByCode1('vi', true); // Việt Nam
62 |
63 | // Get language name from ISO-639-2t code
64 | echo $iso->languageByCode2t('eng'); // English
65 | echo $iso->languageByCode2t('ind'); // Indonesian
66 | echo $iso->languageByCode2t('jav'); // Javanese
67 |
68 | // Get native language name from ISO-639-2t code
69 | echo $iso->nativeByCode2t('eng'); // English
70 | echo $iso->nativeByCode2t('ind'); // Bahasa Indonesia
71 | echo $iso->nativeByCode2t('jav'); // basa Jawa
72 |
73 | // Get native language name from ISO-639-2t code with capitalized
74 | echo $iso->nativeByCode2t('eng', true); // English
75 | echo $iso->nativeByCode2t('ind', true); // Bahasa Indonesia
76 | echo $iso->nativeByCode2t('jav', true); // Basa Jawa
77 | echo $iso->nativeByCode2t('hin', true); // हिन्दी, हिंदी
78 | echo $iso->nativeByCode2t('tha', true); // ไทย
79 | echo $iso->nativeByCode2t('kor', true); // 한국어
80 | echo $iso->nativeByCode2t('jpn', true); // 日本語 (にほんご)
81 | echo $iso->nativeByCode2t('zho', true); // '中文 (Zhōngwén), 汉语, 漢語
82 | echo $iso->nativeByCode2t('rus', true); // Русский
83 | echo $iso->nativeByCode2t('ara', true); // االعربية
84 | echo $iso->nativeByCode2t('vie', true); // Việt Nam
85 |
86 | // Get language name from ISO-639-2b code
87 | echo $iso->languageByCode2b('eng'); // English
88 | echo $iso->languageByCode2b('ind'); // Indonesian
89 | echo $iso->languageByCode2b('jav'); // Javanese
90 |
91 | // Get native language name from ISO-639-2b code
92 | echo $iso->nativeByCode2b('eng'); // English
93 | echo $iso->nativeByCode2b('ind'); // Bahasa Indonesia
94 | echo $iso->nativeByCode2b('jav'); // basa Jawa
95 |
96 | // Get native language name from ISO-639-2b code with capitalized
97 | echo $iso->nativeByCode2b('eng', true); // English
98 | echo $iso->nativeByCode2b('ind', true); // Bahasa Indonesia
99 | echo $iso->nativeByCode2b('jav', true); // Basa Jawa
100 | echo $iso->nativeByCode2b('hin', true); // हिन्दी, हिंदी
101 | echo $iso->nativeByCode2b('tha', true); // ไทย
102 | echo $iso->nativeByCode2b('kor', true); // 한국어
103 | echo $iso->nativeByCode2b('jpn', true); // 日本語 (にほんご)
104 | echo $iso->nativeByCode2b('chi', true); // '中文 (Zhōngwén), 汉语, 漢語
105 | echo $iso->nativeByCode2b('rus', true); // Русский
106 | echo $iso->nativeByCode2b('ara', true); // االعربية
107 | echo $iso->nativeByCode2b('vie', true); // Việt Nam
108 |
109 | // Get language name from ISO-639-3 code
110 | echo $iso->languageByCode3('eng'); // English
111 | echo $iso->languageByCode3('ind'); // Indonesian
112 | echo $iso->languageByCode3('jav'); // Javanese
113 |
114 | // Get native language name from ISO-639-3 code
115 | echo $iso->nativeByCode3('eng'); // English
116 | echo $iso->nativeByCode3('ind'); // Bahasa Indonesia
117 | echo $iso->nativeByCode3('jav'); // basa Jawa
118 |
119 | // Get native language name from ISO-639-3 code with capitalized
120 | echo $iso->nativeByCode3('eng', true); // English
121 | echo $iso->nativeByCode3('ind', true); // Bahasa Indonesia
122 | echo $iso->nativeByCode3('jav', true); // Basa Jawa
123 | echo $iso->nativeByCode3('hin', true); // हिन्दी, हिंदी
124 | echo $iso->nativeByCode3('tha', true); // ไทย
125 | echo $iso->nativeByCode3('kor', true); // 한국어
126 | echo $iso->nativeByCode3('jpn', true); // 日本語 (にほんご)
127 | echo $iso->nativeByCode3('zho', true); // '中文 (Zhōngwén), 汉语, 漢語
128 | echo $iso->nativeByCode3('rus', true); // Русский
129 | echo $iso->nativeByCode3('ara', true); // االعربية
130 | echo $iso->nativeByCode3('vie', true); // Việt Nam
131 |
132 | // Get language array from ISO-639-2b code
133 | echo $iso->getLanguageByIsoCode2b('eng'); // ['en', 'eng', 'eng', 'eng', 'English', 'English']
134 | echo $iso->getLanguageByIsoCode2b('ind'); // ['id', 'ind', 'ind', 'ind', 'Indonesian', 'Bahasa Indonesia']
135 | echo $iso->getLanguageByIsoCode2b('jav'); // ['jv', 'jav', 'jav', 'jav', 'Javanese', 'basa Jawa']
136 | ```
137 |
138 | ## Contributing
139 |
140 | Do as usual contribution on open source projects by creating a pull request!
141 |
142 | ### Install Composer
143 |
144 | ```console
145 | composer install
146 | ```
147 |
148 | ### Run Test
149 |
150 | ```console
151 | composer run-script test
152 | ```
153 |
154 | ### Run Linter Fix
155 |
156 | ```console
157 | composer run-script lint:fix
158 | ```
159 |
--------------------------------------------------------------------------------
/resources/wikipedia.csv:
--------------------------------------------------------------------------------
1 | Northwest Caucasian|Abkhaz|аҧсуа бызшәа, аҧсшәа|ab|abk|abk|abk|abks|
2 | Afro-Asiatic|Afar|Afaraf|aa|aar|aar|aar|aars|
3 | Indo-European|Afrikaans|Afrikaans|af|afr|afr|afr|afrs|
4 | Niger–Congo|Akan|Akan|ak|aka|aka|aka + 2||macrolanguage, Twi is [tw/twi], Fanti is [fat]
5 | Indo-European|Albanian|Shqip|sq|sqi|alb|sqi + 4||macrolanguage, "Albanian Phylozone" in 639-6
6 | Afro-Asiatic|Amharic|አማርኛ|am|amh|amh|amh||
7 | Afro-Asiatic|Arabic|العربية|ar|ara|ara|ara + 30||macrolanguage, Standard Arabic is [arb]
8 | Indo-European|Aragonese|aragonés|an|arg|arg|arg||
9 | Indo-European|Armenian|Հայերեն|hy|hye|arm|hye||
10 | Indo-European|Assamese|অসমীয়া|as|asm|asm|asm||
11 | Northeast Caucasian|Avaric|авар мацӀ, магӀарул мацӀ|av|ava|ava|ava||
12 | Indo-European|Avestan|avesta|ae|ave|ave|ave||ancient
13 | Aymaran|Aymara|aymar aru|ay|aym|aym|aym + 2||macrolanguage
14 | Turkic|Azerbaijani|azərbaycan dili|az|aze|aze|aze + 2||macrolanguage
15 | Niger–Congo|Bambara|bamanankan|bm|bam|bam|bam||
16 | Turkic|Bashkir|башҡорт теле|ba|bak|bak|bak||
17 | Language isolate|Basque|euskara, euskera|eu|eus|baq|eus||
18 | Indo-European|Belarusian|беларуская мова|be|bel|bel|bel||
19 | Indo-European|Bengali, Bangla|বাংলা|bn|ben|ben|ben||
20 | Indo-European|Bihari|भोजपुरी|bh|bih|bih|||Collective language code for Bhojpuri, Magahi, and Maithili
21 | Creole|Bislama|Bislama|bi|bis|bis|bis||
22 | Indo-European|Bosnian|bosanski jezik|bs|bos|bos|bos|boss|
23 | Indo-European|Breton|brezhoneg|br|bre|bre|bre||
24 | Indo-European|Bulgarian|български език|bg|bul|bul|bul|buls|
25 | Sino-Tibetan|Burmese|ဗမာစာ|my|mya|bur|mya||
26 | Indo-European|Catalan|català|ca|cat|cat|cat||
27 | Austronesian|Chamorro|Chamoru|ch|cha|cha|cha||
28 | Northeast Caucasian|Chechen|нохчийн мотт|ce|che|che|che||
29 | Niger–Congo|Chichewa, Chewa, Nyanja|chiCheŵa, chinyanja|ny|nya|nya|nya||
30 | Sino-Tibetan|Chinese|中文 (Zhōngwén), 汉语, 漢語|zh|zho|chi|zho + 13||macrolanguage
31 | Turkic|Chuvash|чӑваш чӗлхи|cv|chv|chv|chv||
32 | Indo-European|Cornish|Kernewek|kw|cor|cor|cor||
33 | Indo-European|Corsican|corsu, lingua corsa|co|cos|cos|cos||
34 | Algonquian|Cree|ᓀᐦᐃᔭᐍᐏᐣ|cr|cre|cre|cre + 6||macrolanguage
35 | Indo-European|Croatian|hrvatski jezik|hr|hrv|hrv|hrv||
36 | Indo-European|Czech|čeština, český jazyk|cs|ces|cze|ces||
37 | Indo-European|Danish|dansk|da|dan|dan|dan||
38 | Indo-European|Divehi, Dhivehi, Maldivian|ދިވެހި|dv|div|div|div||
39 | Indo-European|Dutch|Nederlands, Vlaams|nl|nld|dut|nld||
40 | Sino-Tibetan|Dzongkha|རྫོང་ཁ|dz|dzo|dzo|dzo||
41 | Indo-European|English|English|en|eng|eng|eng|engs|
42 | Constructed|Esperanto|Esperanto|eo|epo|epo|epo||constructed, initiated from L.L. Zamenhof, 1887
43 | Uralic|Estonian|eesti, eesti keel|et|est|est|est + 2||macrolanguage
44 | Niger–Congo|Ewe|Eʋegbe|ee|ewe|ewe|ewe||
45 | Indo-European|Faroese|føroyskt|fo|fao|fao|fao||
46 | Austronesian|Fijian|vosa Vakaviti|fj|fij|fij|fij||
47 | Uralic|Finnish|suomi, suomen kieli|fi|fin|fin|fin||
48 | Indo-European|French|français, langue française|fr|fra|fre|fra|fras|
49 | Niger–Congo|Fula, Fulah, Pulaar, Pular|Fulfulde, Pulaar, Pular|ff|ful|ful|ful + 9||macrolanguage
50 | Indo-European|Galician|galego|gl|glg|glg|glg||
51 | South Caucasian|Georgian|ქართული|ka|kat|geo|kat||
52 | Indo-European|German|Deutsch|de|deu|ger|deu|deus|
53 | Indo-European|Greek (modern)|ελληνικά|el|ell|gre|ell|ells|
54 | Tupian|Guaraní|Avañe'ẽ|gn|grn|grn|grn + 5||macrolanguage
55 | Indo-European|Gujarati|ગુજરાતી|gu|guj|guj|guj||
56 | Creole|Haitian, Haitian Creole|Kreyòl ayisyen|ht|hat|hat|hat||
57 | Afro-Asiatic|Hausa|(Hausa) هَوُسَ|ha|hau|hau|hau||
58 | Afro-Asiatic|Hebrew (modern)|עברית|he|heb|heb|heb||
59 | Niger–Congo|Herero|Otjiherero|hz|her|her|her||
60 | Indo-European|Hindi|हिन्दी, हिंदी|hi|hin|hin|hin|hins|
61 | Austronesian|Hiri Motu|Hiri Motu|ho|hmo|hmo|hmo||
62 | Uralic|Hungarian|magyar|hu|hun|hun|hun||
63 | Constructed|Interlingua|Interlingua|ia|ina|ina|ina||constructed by International Auxiliary Language Association
64 | Austronesian|Indonesian|Bahasa Indonesia|id|ind|ind|ind||Covered by macrolanguage [ms/msa]
65 | Constructed|Interlingue|Originally called Occidental; then Interlingue after WWII|ie|ile|ile|ile||constructed by Edgar de Wahl, first published in 1922
66 | Indo-European|Irish|Gaeilge|ga|gle|gle|gle||
67 | Niger–Congo|Igbo|Asụsụ Igbo|ig|ibo|ibo|ibo||
68 | Eskimo–Aleut|Inupiaq|Iñupiaq, Iñupiatun|ik|ipk|ipk|ipk + 2||macrolanguage
69 | Constructed|Ido|Ido|io|ido|ido|ido|idos|constructed by De Beaufront, 1907, as variation of Esperanto
70 | Indo-European|Icelandic|Íslenska|is|isl|ice|isl||
71 | Indo-European|Italian|italiano|it|ita|ita|ita|itas|
72 | Eskimo–Aleut|Inuktitut|ᐃᓄᒃᑎᑐᑦ|iu|iku|iku|iku + 2||macrolanguage
73 | Japonic|Japanese|日本語 (にほんご)|ja|jpn|jpn|jpn||
74 | Austronesian|Javanese|basa Jawa|jv|jav|jav|jav||
75 | Eskimo–Aleut|Kalaallisut, Greenlandic|kalaallisut, kalaallit oqaasii|kl|kal|kal|kal||
76 | Dravidian|Kannada|ಕನ್ನಡ|kn|kan|kan|kan||
77 | Nilo-Saharan|Kanuri|Kanuri|kr|kau|kau|kau + 3||macrolanguage
78 | Indo-European|Kashmiri|कश्मीरी, كشميري|ks|kas|kas|kas||
79 | Turkic|Kazakh|қазақ тілі|kk|kaz|kaz|kaz||
80 | Austroasiatic|Khmer|ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ|km|khm|khm|khm||a.k.a. Cambodian
81 | Niger–Congo|Kikuyu, Gikuyu|Gĩkũyũ|ki|kik|kik|kik||
82 | Niger–Congo|Kinyarwanda|Ikinyarwanda|rw|kin|kin|kin||
83 | Turkic|Kyrgyz|Кыргызча, Кыргыз тили|ky|kir|kir|kir||
84 | Uralic|Komi|коми кыв|kv|kom|kom|kom + 2||macrolanguage
85 | Niger–Congo|Kongo|Kikongo|kg|kon|kon|kon + 3||macrolanguage
86 | Koreanic|Korean|한국어, 조선어|ko|kor|kor|kor||
87 | Indo-European|Kurdish|Kurdî, كوردی|ku|kur|kur|kur + 3||macrolanguage
88 | Niger–Congo|Kwanyama, Kuanyama|Kuanyama|kj|kua|kua|kua||
89 | Indo-European|Latin|latine, lingua latina|la|lat|lat|lat|lats|ancient
90 | Indo-European|Ladin|ladin, lingua ladina||||lld||
91 | Indo-European|Luxembourgish, Letzeburgesch|Lëtzebuergesch|lb|ltz|ltz|ltz||
92 | Niger–Congo|Ganda|Luganda|lg|lug|lug|lug||
93 | Indo-European|Limburgish, Limburgan, Limburger|Limburgs|li|lim|lim|lim||
94 | Niger–Congo|Lingala|Lingála|ln|lin|lin|lin||
95 | Tai–Kadai|Lao|ພາສາລາວ|lo|lao|lao|lao||
96 | Indo-European|Lithuanian|lietuvių kalba|lt|lit|lit|lit||
97 | Niger–Congo|Luba-Katanga|Tshiluba|lu|lub|lub|lub||
98 | Indo-European|Latvian|latviešu valoda|lv|lav|lav|lav + 2||macrolanguage
99 | Indo-European|Manx|Gaelg, Gailck|gv|glv|glv|glv||
100 | Indo-European|Macedonian|македонски јазик|mk|mkd|mac|mkd||
101 | Austronesian|Malagasy|fiteny malagasy|mg|mlg|mlg|mlg + 10||macrolanguage
102 | Austronesian|Malay|bahasa Melayu, بهاس ملايو|ms|msa|may|msa + 13||macrolanguage, Standard Malay is [zsm], Indonesian is [id/ind]
103 | Dravidian|Malayalam|മലയാളം|ml|mal|mal|mal||
104 | Afro-Asiatic|Maltese|Malti|mt|mlt|mlt|mlt||
105 | Austronesian|Māori|te reo Māori|mi|mri|mao|mri||
106 | Indo-European|Marathi (Marāṭhī)|मराठी|mr|mar|mar|mar||
107 | Austronesian|Marshallese|Kajin M̧ajeļ|mh|mah|mah|mah||
108 | Mongolic|Mongolian|монгол|mn|mon|mon|mon + 2||macrolanguage
109 | Austronesian|Nauru|Ekakairũ Naoero|na|nau|nau|nau||
110 | Dené–Yeniseian|Navajo, Navaho|Diné bizaad|nv|nav|nav|nav||
111 | Niger–Congo|Northern Ndebele|isiNdebele|nd|nde|nde|nde||
112 | Indo-European|Nepali|नेपाली|ne|nep|nep|nep||
113 | Niger–Congo|Ndonga|Owambo|ng|ndo|ndo|ndo||
114 | Indo-European|Norwegian Bokmål|Norsk bokmål|nb|nob|nob|nob||Covered by macrolanguage [no/nor]
115 | Indo-European|Norwegian Nynorsk|Norsk nynorsk|nn|nno|nno|nno||Covered by macrolanguage [no/nor]
116 | Indo-European|Norwegian|Norsk|no|nor|nor|nor + 2||macrolanguage, Bokmål is [nb/nob], Nynorsk is [nn/nno]
117 | Sino-Tibetan|Nuosu|ꆈꌠ꒿ Nuosuhxop|ii|iii|iii|iii||Standard form of Yi languages
118 | Niger–Congo|Southern Ndebele|isiNdebele|nr|nbl|nbl|nbl||
119 | Indo-European|Occitan|occitan, lenga d'òc|oc|oci|oci|oci||
120 | Algonquian|Ojibwe, Ojibwa|ᐊᓂᔑᓈᐯᒧᐎᓐ|oj|oji|oji|oji + 7||macrolanguage
121 | Indo-European|Old Church Slavonic, Church Slavonic, Old Bulgarian|ѩзыкъ словѣньскъ|cu|chu|chu|chu||ancient, in use by Orthodox Church
122 | Afro-Asiatic|Oromo|Afaan Oromoo|om|orm|orm|orm + 4||macrolanguage
123 | Indo-European|Oriya|ଓଡ଼ିଆ|or|ori|ori|ori||
124 | Indo-European|Ossetian, Ossetic|ирон æвзаг|os|oss|oss|oss||
125 | Indo-European|Panjabi, Punjabi|ਪੰਜਾਬੀ, پنجابی|pa|pan|pan|pan||
126 | Indo-European|Pāli|पाऴि|pi|pli|pli|pli||ancient
127 | Indo-European|Persian (Farsi)|فارسی|fa|fas|per|fas + 2||macrolanguage
128 | Indo-European|Polish|język polski, polszczyzna|pl|pol|pol|pol|pols|
129 | Indo-European|Pashto, Pushto|پښتو|ps|pus|pus|pus + 3||macrolanguage
130 | Indo-European|Portuguese|português|pt|por|por|por||
131 | Quechuan|Quechua|Runa Simi, Kichwa|qu|que|que|que + 44||macrolanguage
132 | Indo-European|Romansh|rumantsch grischun|rm|roh|roh|roh||
133 | Niger–Congo|Kirundi|Ikirundi|rn|run|run|run||
134 | Indo-European|Romanian|limba română|ro|ron|rum|ron||[mo] for Moldavian has been withdrawn, recommending [ro] also for Moldavian
135 | Indo-European|Russian|Русский|ru|rus|rus|rus||
136 | Indo-European|Sanskrit (Saṁskṛta)|संस्कृतम्|sa|san|san|san||ancient, still spoken
137 | Indo-European|Sardinian|sardu|sc|srd|srd|srd + 4||macrolanguage
138 | Indo-European|Sindhi|सिन्धी, سنڌي، سندھی|sd|snd|snd|snd||
139 | Uralic|Northern Sami|Davvisámegiella|se|sme|sme|sme||
140 | Austronesian|Samoan|gagana fa'a Samoa|sm|smo|smo|smo||
141 | Creole|Sango|yângâ tî sängö|sg|sag|sag|sag||
142 | Indo-European|Serbian|српски језик|sr|srp|srp|srp||The ISO 639-2/T code srp deprecated the ISO 639-2/B code scc[1]
143 | Indo-European|Scottish Gaelic, Gaelic|Gàidhlig|gd|gla|gla|gla||
144 | Niger–Congo|Shona|chiShona|sn|sna|sna|sna||
145 | Indo-European|Sinhala, Sinhalese|සිංහල|si|sin|sin|sin||
146 | Indo-European|Slovak|slovenčina, slovenský jazyk|sk|slk|slo|slk||
147 | Indo-European|Slovene|slovenski jezik, slovenščina|sl|slv|slv|slv||
148 | Afro-Asiatic|Somali|Soomaaliga, af Soomaali|so|som|som|som||
149 | Niger–Congo|Southern Sotho|Sesotho|st|sot|sot|sot||
150 | Indo-European|Spanish|español|es|spa|spa|spa||
151 | Austronesian|Sundanese|Basa Sunda|su|sun|sun|sun||
152 | Niger–Congo|Swahili|Kiswahili|sw|swa|swa|swa + 2||macrolanguage
153 | Niger–Congo|Swati|SiSwati|ss|ssw|ssw|ssw||
154 | Indo-European|Swedish|svenska|sv|swe|swe|swe||
155 | Dravidian|Tamil|தமிழ்|ta|tam|tam|tam||
156 | Dravidian|Telugu|తెలుగు|te|tel|tel|tel||
157 | Indo-European|Tajik|тоҷикӣ, toçikī, تاجیکی|tg|tgk|tgk|tgk||
158 | Tai–Kadai|Thai|ไทย|th|tha|tha|tha||
159 | Afro-Asiatic|Tigrinya|ትግርኛ|ti|tir|tir|tir||
160 | Sino-Tibetan|Tibetan Standard, Tibetan, Central|བོད་ཡིག|bo|bod|tib|bod||
161 | Turkic|Turkmen|Türkmen, Түркмен|tk|tuk|tuk|tuk||
162 | Austronesian|Tagalog|Wikang Tagalog, ᜏᜒᜃᜅ᜔ ᜆᜄᜎᜓᜄ᜔|tl|tgl|tgl|tgl||Note: Filipino (Pilipino) has the code [fil]
163 | Niger–Congo|Tswana|Setswana|tn|tsn|tsn|tsn||
164 | Austronesian|Tonga (Tonga Islands)|faka Tonga|to|ton|ton|ton||
165 | Turkic|Turkish|Türkçe|tr|tur|tur|tur||
166 | Niger–Congo|Tsonga|Xitsonga|ts|tso|tso|tso||
167 | Turkic|Tatar|татар теле, tatar tele|tt|tat|tat|tat||
168 | Niger–Congo|Twi|Twi|tw|twi|twi|twi||Covered by macrolanguage [ak/aka]
169 | Austronesian|Tahitian|Reo Tahiti|ty|tah|tah|tah||One of the Reo Mā`ohi (languages of French Polynesia)
170 | Turkic|Uyghur|ئۇيغۇرچە, Uyghurche|ug|uig|uig|uig||
171 | Indo-European|Ukrainian|українська мова|uk|ukr|ukr|ukr||
172 | Indo-European|Urdu|اردو|ur|urd|urd|urd||
173 | Turkic|Uzbek|Oʻzbek, Ўзбек, أۇزبېك|uz|uzb|uzb|uzb + 2||macrolanguage
174 | Niger–Congo|Venda|Tshivenḓa|ve|ven|ven|ven||
175 | Austroasiatic|Vietnamese|Việt Nam|vi|vie|vie|vie||
176 | Constructed|Volapük|Volapük|vo|vol|vol|vol||constructed
177 | Indo-European|Walloon|walon|wa|wln|wln|wln||
178 | Indo-European|Welsh|Cymraeg|cy|cym|wel|cym||
179 | Niger–Congo|Wolof|Wollof|wo|wol|wol|wol||
180 | Indo-European|Western Frisian|Frysk|fy|fry|fry|fry||
181 | Niger–Congo|Xhosa|isiXhosa|xh|xho|xho|xho||
182 | Indo-European|Yiddish|ייִדיש|yi|yid|yid|yid + 2||macrolanguage
183 | Niger–Congo|Yoruba|Yorùbá|yo|yor|yor|yor||
184 | Tai–Kadai|Zhuang, Chuang|Saɯ cueŋƅ, Saw cuengh|za|zha|zha|zha + 16||macrolanguage
185 | Niger–Congo|Zulu|isiZulu|zu|zul|zul|zul||
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | workflow_run:
5 | workflows:
6 | - Run Test
7 | types:
8 | - completed
9 | branches:
10 | - master
11 | workflow_dispatch:
12 |
13 | permissions:
14 | contents: write
15 | pull-requests: read
16 |
17 | jobs:
18 | create-release:
19 | runs-on: ubuntu-latest
20 | # Only run if the test workflow completed successfully
21 | if: github.event.workflow_run.conclusion == 'success'
22 |
23 | steps:
24 | - name: Checkout code
25 | uses: actions/checkout@v5
26 | with:
27 | fetch-depth: 0
28 | token: ${{ secrets.GITHUB_TOKEN }}
29 | ref: ${{ github.event.workflow_run.head_sha }}
30 |
31 | - name: Check if triggered by PR merge to master
32 | id: check-trigger
33 | run: |
34 | # Check if this was triggered by a PR merge to master
35 | # workflow_run events don't have direct PR info, so we check the commit message
36 | COMMIT_MESSAGE=$(git log -1 --pretty=%B)
37 | echo "Commit message: $COMMIT_MESSAGE"
38 |
39 | # Extract PR number from merge commit message
40 | if echo "$COMMIT_MESSAGE" | grep -q "Merge pull request"; then
41 | echo "Triggered by PR merge to master"
42 | echo "is-pr-merge=true" >> $GITHUB_OUTPUT
43 |
44 | # Extract PR number from commit message like "Merge pull request #123 from..."
45 | PR_NUMBER=$(echo "$COMMIT_MESSAGE" | sed -n 's/.*Merge pull request #\([0-9]*\).*/\1/p')
46 | echo "pr-number=$PR_NUMBER" >> $GITHUB_OUTPUT
47 | echo "Found PR number: $PR_NUMBER"
48 | else
49 | echo "Not triggered by PR merge, skipping release"
50 | echo "is-pr-merge=false" >> $GITHUB_OUTPUT
51 | fi
52 |
53 | - name: Get PR description
54 | id: get-pr-description
55 | if: steps.check-trigger.outputs.is-pr-merge == 'true'
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | run: |
59 | # Install jq if not available
60 | which jq || sudo apt-get update && sudo apt-get install -y jq
61 |
62 | PR_NUMBER="${{ steps.check-trigger.outputs.pr-number }}"
63 |
64 | if [ -n "$PR_NUMBER" ]; then
65 | echo "Fetching PR #$PR_NUMBER description..."
66 |
67 | # Get PR description using GitHub API
68 | PR_DESCRIPTION=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
69 | "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | \
70 | jq -r '.body // "No description provided"')
71 |
72 | echo "PR Description:"
73 | echo "$PR_DESCRIPTION"
74 |
75 | # Save PR description for later use
76 | echo "pr-description<> $GITHUB_OUTPUT
77 | echo "$PR_DESCRIPTION" >> $GITHUB_OUTPUT
78 | echo "EOF" >> $GITHUB_OUTPUT
79 | else
80 | echo "No PR number found"
81 | echo "pr-description=No description available" >> $GITHUB_OUTPUT
82 | fi
83 |
84 | - name: Check if changes are only in workflow files
85 | id: check-changes
86 | if: steps.check-trigger.outputs.is-pr-merge == 'true'
87 | run: |
88 | # Get the list of changed files in the last commit
89 | CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)
90 |
91 | # Check if all changed files are in .github/workflows/ directory
92 | WORKFLOW_ONLY=true
93 | for file in $CHANGED_FILES; do
94 | if [[ ! "$file" =~ ^\.github/workflows/ ]]; then
95 | WORKFLOW_ONLY=false
96 | break
97 | fi
98 | done
99 |
100 | echo "workflow-only=$WORKFLOW_ONLY" >> $GITHUB_OUTPUT
101 | echo "Changed files: $CHANGED_FILES"
102 |
103 | # If only workflow files changed, skip release
104 | if [ "$WORKFLOW_ONLY" = "true" ]; then
105 | echo "Only workflow files changed, skipping release"
106 | echo "skip-release=true" >> $GITHUB_OUTPUT
107 | else
108 | echo "Non-workflow files changed, proceeding with release"
109 | echo "skip-release=false" >> $GITHUB_OUTPUT
110 | fi
111 |
112 | - name: Get latest release
113 | id: get-latest-release
114 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false'
115 | run: |
116 | # Get the latest release tag
117 | LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
118 | if [ -z "$LATEST_TAG" ]; then
119 | echo "No previous releases found, starting from v0.0.0"
120 | echo "latest-tag=v0.0.0" >> $GITHUB_OUTPUT
121 | echo "is-first-release=true" >> $GITHUB_OUTPUT
122 | else
123 | echo "Latest tag: $LATEST_TAG"
124 | echo "latest-tag=$LATEST_TAG" >> $GITHUB_OUTPUT
125 | echo "is-first-release=false" >> $GITHUB_OUTPUT
126 | fi
127 |
128 | - name: Determine version bump
129 | id: version-bump
130 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false'
131 | run: |
132 | # Get the branch name from the PR
133 | PR_NUMBER="${{ steps.check-trigger.outputs.pr-number }}"
134 | BRANCH_NAME=""
135 |
136 | if [ -n "$PR_NUMBER" ]; then
137 | # Get branch name from GitHub API
138 | BRANCH_NAME=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
139 | "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | \
140 | jq -r '.head.ref // ""')
141 | echo "Branch name: $BRANCH_NAME"
142 | fi
143 |
144 | # Initialize version bump flags
145 | MAJOR_BUMP=false
146 | MINOR_BUMP=false
147 | PATCH_BUMP=false
148 |
149 | # Check branch name for explicit version bump indicators
150 | if [ -n "$BRANCH_NAME" ]; then
151 | echo "Analyzing branch name: $BRANCH_NAME"
152 |
153 | # Convert to lowercase for case-insensitive matching
154 | BRANCH_LOWER=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]')
155 |
156 | # Check for major version indicators in branch name
157 | if echo "$BRANCH_LOWER" | grep -qE "(major|refactor|rewrite|breaking)"; then
158 | echo "🔴 MAJOR version bump detected from branch name"
159 | MAJOR_BUMP=true
160 | # Check for minor version indicators in branch name
161 | elif echo "$BRANCH_LOWER" | grep -qE "(minor|feature|feat|new)"; then
162 | echo "🟡 MINOR version bump detected from branch name"
163 | MINOR_BUMP=true
164 | # Check for patch version indicators in branch name
165 | elif echo "$BRANCH_LOWER" | grep -qE "(patch|fix|bug|hotfix|chore|docs|style|test)"; then
166 | echo "🟢 PATCH version bump detected from branch name"
167 | PATCH_BUMP=true
168 | else
169 | # If no keywords found in branch name, default to patch
170 | echo "🟢 No version keywords found in branch name, defaulting to PATCH version bump"
171 | PATCH_BUMP=true
172 | fi
173 | else
174 | # If no branch name found, default to patch
175 | echo "🟢 No branch name found, defaulting to PATCH version bump"
176 | PATCH_BUMP=true
177 | fi
178 |
179 | echo "Version bump decision:"
180 | echo " Branch name: $BRANCH_NAME"
181 | echo " Major bump: $MAJOR_BUMP"
182 | echo " Minor bump: $MINOR_BUMP"
183 | echo " Patch bump: $PATCH_BUMP"
184 |
185 | echo "major-bump=$MAJOR_BUMP" >> $GITHUB_OUTPUT
186 | echo "minor-bump=$MINOR_BUMP" >> $GITHUB_OUTPUT
187 | echo "patch-bump=$PATCH_BUMP" >> $GITHUB_OUTPUT
188 |
189 | - name: Calculate new version
190 | id: calculate-version
191 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false'
192 | run: |
193 | LATEST_TAG="${{ steps.get-latest-release.outputs.latest-tag }}"
194 |
195 | # Extract version numbers
196 | if [ "${{ steps.get-latest-release.outputs.is-first-release }}" = "true" ]; then
197 | NEW_VERSION="v1.0.0"
198 | else
199 | # Remove 'v' prefix and split version
200 | VERSION=${LATEST_TAG#v}
201 | IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
202 |
203 | if [ "${{ steps.version-bump.outputs.major-bump }}" = "true" ]; then
204 | MAJOR=$((MAJOR + 1))
205 | MINOR=0
206 | PATCH=0
207 | elif [ "${{ steps.version-bump.outputs.minor-bump }}" = "true" ]; then
208 | MINOR=$((MINOR + 1))
209 | PATCH=0
210 | elif [ "${{ steps.version-bump.outputs.patch-bump }}" = "true" ]; then
211 | PATCH=$((PATCH + 1))
212 | fi
213 |
214 | NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
215 | fi
216 |
217 | echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
218 | echo "New version: $NEW_VERSION"
219 |
220 | - name: Generate release description
221 | id: release-description
222 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false'
223 | run: |
224 | PR_DESCRIPTION="${{ steps.get-pr-description.outputs.pr-description }}"
225 | LATEST_TAG="${{ steps.get-latest-release.outputs.latest-tag }}"
226 | NEW_VERSION="${{ steps.calculate-version.outputs.new-version }}"
227 |
228 | # Create release description with PR description and changelog
229 | echo "## Release Notes" > RELEASE_DESCRIPTION.md
230 | echo "" >> RELEASE_DESCRIPTION.md
231 |
232 | if [ "$PR_DESCRIPTION" != "No description provided" ] && [ "$PR_DESCRIPTION" != "No description available" ] && [ -n "$PR_DESCRIPTION" ]; then
233 | echo "$PR_DESCRIPTION" >> RELEASE_DESCRIPTION.md
234 | echo "" >> RELEASE_DESCRIPTION.md
235 | fi
236 |
237 | # Add version bump information
238 | echo "## Version Information" >> RELEASE_DESCRIPTION.md
239 | echo "" >> RELEASE_DESCRIPTION.md
240 | if [ "${{ steps.version-bump.outputs.major-bump }}" = "true" ]; then
241 | echo "🔴 **Major Release** - Contains breaking changes or major refactoring" >> RELEASE_DESCRIPTION.md
242 | elif [ "${{ steps.version-bump.outputs.minor-bump }}" = "true" ]; then
243 | echo "🟡 **Minor Release** - Contains new features" >> RELEASE_DESCRIPTION.md
244 | elif [ "${{ steps.version-bump.outputs.patch-bump }}" = "true" ]; then
245 | echo "🟢 **Patch Release** - Contains bug fixes and improvements" >> RELEASE_DESCRIPTION.md
246 | fi
247 | echo "" >> RELEASE_DESCRIPTION.md
248 |
249 | echo "## Changes" >> RELEASE_DESCRIPTION.md
250 | echo "" >> RELEASE_DESCRIPTION.md
251 |
252 | # Generate changelog from commits
253 | if [ "${{ steps.get-latest-release.outputs.is-first-release }}" = "true" ]; then
254 | COMMITS=$(git log --oneline --no-merges --reverse)
255 | echo "### Initial Release" >> RELEASE_DESCRIPTION.md
256 | echo "" >> RELEASE_DESCRIPTION.md
257 | echo "$COMMITS" >> RELEASE_DESCRIPTION.md
258 | else
259 | COMMITS=$(git log --oneline --no-merges --reverse ${LATEST_TAG}..HEAD)
260 | echo "$COMMITS" >> RELEASE_DESCRIPTION.md
261 | fi
262 |
263 | # Read release description for output
264 | RELEASE_DESC=$(cat RELEASE_DESCRIPTION.md)
265 | echo "release-description<> $GITHUB_OUTPUT
266 | echo "$RELEASE_DESC" >> $GITHUB_OUTPUT
267 | echo "EOF" >> $GITHUB_OUTPUT
268 |
269 | echo "Final release description:"
270 | echo "$RELEASE_DESC"
271 |
272 | - name: Create Release
273 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'false'
274 | uses: softprops/action-gh-release@v1
275 | env:
276 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
277 | with:
278 | tag_name: ${{ steps.calculate-version.outputs.new-version }}
279 | name: ${{ steps.calculate-version.outputs.new-version }}
280 | body: ${{ steps.release-description.outputs.release-description }}
281 | draft: false
282 | prerelease: false
283 |
284 | - name: Skip Release - Not PR Merge
285 | if: steps.check-trigger.outputs.is-pr-merge == 'false'
286 | run: |
287 | echo "Skipping release as this was not triggered by a PR merge"
288 |
289 | - name: Skip Release - Workflow Only
290 | if: steps.check-trigger.outputs.is-pr-merge == 'true' && steps.check-changes.outputs.skip-release == 'true'
291 | run: |
292 | echo "Skipping release as only workflow files were changed"
293 |
--------------------------------------------------------------------------------
/src/ISO639.php:
--------------------------------------------------------------------------------
1 | hasMbstring = extension_loaded('mbstring');
225 |
226 | $this->buildHashmap();
227 | }
228 |
229 | /**
230 | * Convert string to lowercase using mbstring if available, fallback to strtolower
231 | */
232 | private function toLower(string $string): string
233 | {
234 | $string = trim($string);
235 |
236 | if ($this->hasMbstring) {
237 | return mb_strtolower($string, 'UTF-8');
238 | }
239 |
240 | return strtolower($string);
241 | }
242 |
243 | /**
244 | * Convert first character of each word to uppercase using mbstring if available, fallback to ucwords
245 | */
246 | private function toTitleCase(string $string): string
247 | {
248 | if ($this->hasMbstring) {
249 | return mb_convert_case($string, MB_CASE_TITLE, 'UTF-8');
250 | }
251 |
252 | return ucwords($string);
253 | }
254 |
255 | private array $iso639_1 = [];
256 | private array $iso639_2t = [];
257 | private array $iso639_2b = [];
258 | private array $iso639_3 = [];
259 | private array $langEnglish = [];
260 | private array $code2tToCode1 = [];
261 | private array $code1ToCode2t = [];
262 | private array $code2bToLang = [];
263 |
264 | private function buildHashmap(): void
265 | {
266 | foreach ($this->languages as $lang) {
267 | $iso639_1 = $this->toLower($lang[self::INDEX_ISO639_1]);
268 | $iso639_2t = $this->toLower($lang[self::INDEX_ISO639_2T]);
269 | $iso639_2b = $this->toLower($lang[self::INDEX_ISO639_2B]);
270 | $iso639_3 = $this->toLower($lang[self::INDEX_ISO639_3]);
271 |
272 | $english = $lang[self::INDEX_ENGLISH_NAME];
273 | $native = $lang[self::INDEX_NATIVE_NAME];
274 |
275 | $val = [
276 | self::KEY_CODE_1 => $iso639_1,
277 | self::KEY_CODE_2T => $iso639_2t,
278 | self::KEY_CODE_2B => $iso639_2b,
279 | self::KEY_CODE_3 => $iso639_3,
280 | self::KEY_ENGLISH => $english,
281 | self::KEY_NATIVE => $native,
282 | ];
283 |
284 | if (!empty($iso639_1)) {
285 | $this->iso639_1[$iso639_1] = $val;
286 | }
287 | if (!empty($iso639_2t)) {
288 | $this->iso639_2t[$iso639_2t] = $val;
289 | }
290 | if (!empty($iso639_2b)) {
291 | $this->iso639_2b[$iso639_2b] = $val;
292 |
293 | $this->code2bToLang[$iso639_2b] = $lang;
294 | }
295 | if (!empty($iso639_3)) {
296 | $this->iso639_3[$iso639_3] = $val;
297 | }
298 |
299 | $this->langEnglish[$this->toLower($english)] = $val;
300 |
301 | if (!empty($iso639_2t) && !empty($iso639_1)) {
302 | $this->code2tToCode1[$iso639_2t] = $iso639_1;
303 | }
304 | if (!empty($iso639_1) && !empty($iso639_2t)) {
305 | $this->code1ToCode2t[$iso639_1] = $iso639_2t;
306 | }
307 | }
308 | }
309 |
310 | /*
311 | * Get all language data
312 | */
313 | public function allLanguages(): array
314 | {
315 | return $this->languages;
316 | }
317 |
318 | /*
319 | * Get language name from ISO-639-1 (two-letters code)
320 | */
321 | public function languageByCode1(string $code): string
322 | {
323 | return $this->iso639_1[$this->toLower($code)][self::KEY_ENGLISH] ?? '';
324 | }
325 |
326 | /*
327 | * Get native language name from ISO-639-1 (two-letters code)
328 | */
329 | public function nativeByCode1(string $code, bool $isCapitalized = false): string
330 | {
331 | $native = $this->iso639_1[$this->toLower($code)][self::KEY_NATIVE] ?? '';
332 |
333 | return $isCapitalized ? $this->toTitleCase($native) : $native;
334 | }
335 |
336 | /*
337 | * Get language name from ISO-639-2/t (three-letter codes) terminologic
338 | */
339 | public function languageByCode2t(string $code): string
340 | {
341 | return $this->iso639_2t[$this->toLower($code)][self::KEY_ENGLISH] ?? '';
342 | }
343 |
344 | /*
345 | * Get native language name from ISO-639-2/t (three-letter codes) terminologic
346 | */
347 | public function nativeByCode2t(string $code, bool $isCapitalized = false): string
348 | {
349 | $native = $this->iso639_2t[$this->toLower($code)][self::KEY_NATIVE] ?? '';
350 |
351 | return $isCapitalized ? $this->toTitleCase($native) : $native;
352 | }
353 |
354 | /*
355 | * Get language name from ISO-639-2/b (three-letter codes) bibliographic
356 | */
357 | public function languageByCode2b(string $code): string
358 | {
359 | return $this->iso639_2b[$this->toLower($code)][self::KEY_ENGLISH] ?? '';
360 | }
361 |
362 | /*
363 | * Get native language name from ISO-639-2/b (three-letter codes) bibliographic
364 | */
365 | public function nativeByCode2b(string $code, bool $isCapitalized = false): string
366 | {
367 | $native = $this->iso639_2b[$this->toLower($code)][self::KEY_NATIVE] ?? '';
368 |
369 | return $isCapitalized ? $this->toTitleCase($native) : $native;
370 | }
371 |
372 | /*
373 | * Get language name from ISO-639-3 (three-letter codes)
374 | */
375 | public function languageByCode3($code): string
376 | {
377 | return $this->iso639_3[$this->toLower($code)][self::KEY_ENGLISH] ?? '';
378 | }
379 |
380 | /*
381 | * Get native language name from ISO-639-3 (three-letter codes)
382 | */
383 | public function nativeByCode3(string $code, bool $isCapitalized = false): string
384 | {
385 | $native = $this->iso639_3[$this->toLower($code)][self::KEY_NATIVE] ?? '';
386 |
387 | return $isCapitalized ? $this->toTitleCase($native) : $native;
388 | }
389 |
390 | /*
391 | * Get ISO-639-1 (two-letters code) from language name
392 | */
393 | public function code1ByLanguage(string $language): string
394 | {
395 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_1] ?? '';
396 | }
397 |
398 | /*
399 | * Get ISO-639-2/t (three-letter codes) terminologic from language name
400 | */
401 | public function code2tByLanguage(string $language): string
402 | {
403 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_2T] ?? '';
404 | }
405 |
406 | /*
407 | * Get ISO-639-2/b (three-letter codes) bibliographic from language name
408 | */
409 | public function code2bByLanguage(string $language): string
410 | {
411 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_2B] ?? '';
412 | }
413 |
414 | /*
415 | * Get ISO-639-3 (three-letter codes) from language name
416 | */
417 | public function code3ByLanguage(string $language): string
418 | {
419 | return $this->langEnglish[$this->toLower($language)][self::KEY_CODE_3] ?? '';
420 | }
421 |
422 | /**
423 | * Gat language array from ISO-639-2/b (three-letter code)
424 | */
425 | public function getLanguageByIsoCode2b(string $code): ?array
426 | {
427 | return $this->code2bToLang[$this->toLower($code)] ?? null;
428 | }
429 |
430 | /**
431 | * Get ISO-639-2t code from ISO-639-1 code
432 | */
433 | public function code2tByCode1(string $code): string
434 | {
435 | return $this->code1ToCode2t[$this->toLower($code)] ?? '';
436 | }
437 |
438 | }
439 |
--------------------------------------------------------------------------------
/tests/ISO639Test.php:
--------------------------------------------------------------------------------
1 | iso = new ISO639();
13 | }
14 |
15 | public static function languageByCode1DataProvider(): array
16 | {
17 | return [
18 | // Happy path
19 | ['en', 'English'],
20 | ['fr', 'French'],
21 | ['es', 'Spanish'],
22 | ['id', 'Indonesian'],
23 | ['jv', 'Javanese'],
24 | ['hi', 'Hindi'],
25 | ['th', 'Thai'],
26 | ['ko', 'Korean'],
27 | ['ja', 'Japanese'],
28 | ['zh', 'Chinese'],
29 | ['ru', 'Russian'],
30 | ['ar', 'Arabic'],
31 | ['vi', 'Vietnamese'],
32 | ['ms', 'Malay'],
33 | ['su', 'Sundanese'],
34 |
35 | // Edge cases with spaces and tabs/newlines
36 | [' en ', 'English'],
37 | [' fr ', 'French'],
38 | ["\tes\t", 'Spanish'],
39 | ["\nid\n", 'Indonesian'],
40 |
41 | // Edge cases with multi case
42 | ['EN', 'English'],
43 | ['Fr', 'French'],
44 | ['eS', 'Spanish'],
45 | ['iD', 'Indonesian'],
46 |
47 | // Invalid
48 | ['xx', ''],
49 | ['abc', ''],
50 | ['eng', ''],
51 |
52 | // Empty
53 | ['', ''],
54 | ];
55 | }
56 |
57 | /** @dataProvider languageByCode1DataProvider */
58 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode1DataProvider')]
59 | public function testLanguageByCode1(string $code, string $expected): void
60 | {
61 | $this->assertSame($expected, $this->iso->languageByCode1($code));
62 | }
63 |
64 | public static function nativeByCode1DataProvider(): array
65 | {
66 | return [
67 | // Default not capitalized
68 | ['en', 'English', false],
69 | ['fr', 'français, langue française', false],
70 | ['es', 'español', false],
71 | ['id', 'Bahasa Indonesia', false],
72 | ['jv', 'basa Jawa', false],
73 | ['hi', 'हिन्दी, हिंदी', false],
74 | ['th', 'ไทย', false],
75 | ['ko', '한국어', false],
76 | ['ja', '日本語 (にほんご)', false],
77 | ['zh', '中文 (Zhōngwén), 汉语, 漢語', false],
78 | ['ru', 'Русский', false],
79 | ['ar', 'العربية', false],
80 | ['vi', 'Việt Nam', false],
81 | ['ms', 'bahasa Melayu, بهاس ملايو', false],
82 | ['su', 'Basa Sunda', false],
83 |
84 | // Capitalized
85 | ['en', 'English', true],
86 | ['fr', 'Français, Langue Française', true],
87 | ['es', 'Español', true],
88 | ['id', 'Bahasa Indonesia', true],
89 | ['jv', 'Basa Jawa', true],
90 | ['hi', 'हिन्दी, हिंदी', true],
91 | ['th', 'ไทย', true],
92 | ['ko', '한국어', true],
93 | ['ja', '日本語 (にほんご)', true],
94 | ['zh', '中文 (Zhōngwén), 汉语, 漢語', true],
95 | ['ru', 'Русский', true],
96 | ['ar', 'العربية', true],
97 | ['vi', 'Việt Nam', true],
98 | ['ms', 'Bahasa Melayu, بهاس ملايو', true],
99 | ['su', 'Basa Sunda', true],
100 |
101 | // Edge cases with spaces and tabs/newlines
102 | [' en ', 'English', false],
103 | [' fr ', 'français, langue française', false],
104 | ["\tes\t", 'español', false],
105 | ["\nid\n", 'Bahasa Indonesia', false],
106 |
107 | [' en ', 'English', true],
108 | [' fr ', 'Français, Langue Française', true],
109 | ["\tes\t", 'Español', true],
110 | ["\nid\n", 'Bahasa Indonesia', true],
111 |
112 | // Edge cases with multi case
113 | ['EN', 'English', false],
114 | ['Fr', 'français, langue française', false],
115 | ['eS', 'español', false],
116 | ['iD', 'Bahasa Indonesia', false],
117 |
118 | ['EN', 'English', true],
119 | ['Fr', 'Français, Langue Française', true],
120 | ['eS', 'Español', true],
121 | ['iD', 'Bahasa Indonesia', true],
122 |
123 | // Invalid
124 | ['xx', '', false],
125 | ['abc', '', false],
126 | ['eng', '', false],
127 |
128 | ['xx', '', true],
129 | ['abc', '', true],
130 | ['eng', '', true],
131 |
132 | // Empty
133 | ['', '', false],
134 | ['', '', true],
135 | ];
136 | }
137 |
138 | /** @dataProvider nativeByCode1DataProvider */
139 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode1DataProvider')]
140 | public function testNativeByCode1(string $code, string $expected, bool $sCapitalized): void
141 | {
142 | $this->assertSame($expected, $this->iso->nativeByCode1($code, $sCapitalized));
143 | }
144 |
145 | public static function languageByCode2tDataProvider(): array
146 | {
147 | return [
148 | // Happy path
149 | ['eng', 'English'],
150 | ['fra', 'French'],
151 | ['spa', 'Spanish'],
152 | ['ind', 'Indonesian'],
153 | ['jav', 'Javanese'],
154 | ['hin', 'Hindi'],
155 | ['tha', 'Thai'],
156 | ['kor', 'Korean'],
157 | ['jpn', 'Japanese'],
158 | ['zho', 'Chinese'],
159 | ['rus', 'Russian'],
160 | ['ara', 'Arabic'],
161 | ['vie', 'Vietnamese'],
162 | ['msa', 'Malay'],
163 | ['sun', 'Sundanese'],
164 |
165 | // Edge cases with spaces and tabs/newlines
166 | [' zho ', 'Chinese'],
167 | [' msa ', 'Malay'],
168 | ["\tzho\t", 'Chinese'],
169 | ["\nmsa\n", 'Malay'],
170 |
171 | // Edge cases with multi case
172 | ['ENG', 'English'],
173 | ['Fra', 'French'],
174 | ['sPA', 'Spanish'],
175 | ['iND', 'Indonesian'],
176 |
177 | // Invalid
178 | ['xxx', ''],
179 | ['abc', ''],
180 | ['en', ''],
181 |
182 | // Empty
183 | ['', ''],
184 | ];
185 | }
186 |
187 | /** @dataProvider languageByCode2tDataProvider */
188 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode2tDataProvider')]
189 | public function testLanguageISO6392t(string $code, string $expected): void
190 | {
191 | $this->assertSame($expected, $this->iso->languageByCode2t($code));
192 | }
193 |
194 | public static function nativeByCode2tDataProvider(): array
195 | {
196 | return [
197 | // Default not capitalized
198 | ['eng', 'English', false],
199 | ['fra', 'français, langue française', false],
200 | ['spa', 'español', false],
201 | ['ind', 'Bahasa Indonesia', false],
202 | ['jav', 'basa Jawa', false],
203 | ['hin', 'हिन्दी, हिंदी', false],
204 | ['tha', 'ไทย', false],
205 | ['kor', '한국어', false],
206 | ['jpn', '日本語 (にほんご)', false],
207 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', false],
208 | ['rus', 'Русский', false],
209 | ['ara', 'العربية', false],
210 | ['vie', 'Việt Nam', false],
211 | ['msa', 'bahasa Melayu, بهاس ملايو', false],
212 | ['sun', 'Basa Sunda', false],
213 |
214 | // Capitalized
215 | ['eng', 'English', true],
216 | ['fra', 'Français, Langue Française', true],
217 | ['spa', 'Español', true],
218 | ['ind', 'Bahasa Indonesia', true],
219 | ['jav', 'Basa Jawa', true],
220 | ['hin', 'हिन्दी, हिंदी', true],
221 | ['tha', 'ไทย', true],
222 | ['kor', '한국어', true],
223 | ['jpn', '日本語 (にほんご)', true],
224 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', true],
225 | ['rus', 'Русский', true],
226 | ['ara', 'العربية', true],
227 | ['vie', 'Việt Nam', true],
228 | ['msa', 'Bahasa Melayu, بهاس ملايو', true],
229 | ['sun', 'Basa Sunda', true],
230 |
231 | // Edge cases with spaces and tabs/newlines
232 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', false],
233 | [' msa ', 'bahasa Melayu, بهاس ملايو', false],
234 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', false],
235 | ["\nmsa\n", 'bahasa Melayu, بهاس ملايو', false],
236 |
237 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', true],
238 | [' msa ', 'Bahasa Melayu, بهاس ملايو', true],
239 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', true],
240 | ["\nmsa\n", 'Bahasa Melayu, بهاس ملايو', true],
241 |
242 | // Edge cases with multi case
243 | ['ENG', 'English', false],
244 | ['Fra', 'français, langue française', false],
245 | ['sPA', 'español', false],
246 | ['iND', 'Bahasa Indonesia', false],
247 |
248 | ['ENG', 'English', true],
249 | ['Fra', 'Français, Langue Française', true],
250 | ['sPA', 'Español', true],
251 | ['iND', 'Bahasa Indonesia', true],
252 |
253 | // Invalid
254 | ['xxx', '', false],
255 | ['abc', '', false],
256 | ['en', '', false],
257 |
258 | ['xxx', '', true],
259 | ['abc', '', true],
260 | ['en', '', true],
261 |
262 | // Empty
263 | ['', '', false],
264 | ['', '', true],
265 | ];
266 | }
267 |
268 | /** @dataProvider nativeByCode2tDataProvider */
269 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode2tDataProvider')]
270 | public function testNativeISO6392t(string $code, string $expected, bool $isCapitalized): void
271 | {
272 | $this->assertSame($expected, $this->iso->nativeByCode2t($code, $isCapitalized));
273 | }
274 |
275 | public static function languageByCode2bDataProvider(): array
276 | {
277 | return [
278 | // Happy path
279 | ['eng', 'English'],
280 | ['fre', 'French'],
281 | ['spa', 'Spanish'],
282 | ['ind', 'Indonesian'],
283 | ['jav', 'Javanese'],
284 | ['hin', 'Hindi'],
285 | ['tha', 'Thai'],
286 | ['kor', 'Korean'],
287 | ['jpn', 'Japanese'],
288 | ['chi', 'Chinese'],
289 | ['rus', 'Russian'],
290 | ['ara', 'Arabic'],
291 | ['vie', 'Vietnamese'],
292 | ['may', 'Malay'],
293 | ['sun', 'Sundanese'],
294 |
295 | // Edge cases with spaces and tabs/newlines
296 | [' chi ', 'Chinese'],
297 | [' may ', 'Malay'],
298 | ["\tchi\t", 'Chinese'],
299 | ["\nmay\n", 'Malay'],
300 |
301 | // Edge cases with multi case
302 | ['ENG', 'English'],
303 | ['Fre', 'French'],
304 | ['sPA', 'Spanish'],
305 | ['iND', 'Indonesian'],
306 |
307 | // Invalid
308 | ['xxx', ''],
309 | ['abc', ''],
310 | ['en', ''],
311 |
312 | // Empty
313 | ['', ''],
314 | ];
315 | }
316 |
317 | /** @dataProvider languageByCode2bDataProvider */
318 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode2bDataProvider')]
319 | public function testLanguageByCode2b(string $code, string $expected): void
320 | {
321 | $this->assertSame($expected, $this->iso->languageByCode2b($code));
322 | }
323 |
324 | public static function nativeByCode2bDataProvider(): array
325 | {
326 | return [
327 | // Default not capitalized
328 | ['eng', 'English', false],
329 | ['fre', 'français, langue française', false],
330 | ['spa', 'español', false],
331 | ['ind', 'Bahasa Indonesia', false],
332 | ['jav', 'basa Jawa', false],
333 | ['hin', 'हिन्दी, हिंदी', false],
334 | ['tha', 'ไทย', false],
335 | ['kor', '한국어', false],
336 | ['jpn', '日本語 (にほんご)', false],
337 | ['chi', '中文 (Zhōngwén), 汉语, 漢語', false],
338 | ['rus', 'Русский', false],
339 | ['ara', 'العربية', false],
340 | ['vie', 'Việt Nam', false],
341 | ['may', 'bahasa Melayu, بهاس ملايو', false],
342 | ['sun', 'Basa Sunda', false],
343 |
344 | // Capitalized
345 | ['eng', 'English', true],
346 | ['fre', 'Français, Langue Française', true],
347 | ['spa', 'Español', true],
348 | ['ind', 'Bahasa Indonesia', true],
349 | ['jav', 'Basa Jawa', true],
350 | ['hin', 'हिन्दी, हिंदी', true],
351 | ['tha', 'ไทย', true],
352 | ['kor', '한국어', true],
353 | ['jpn', '日本語 (にほんご)', true],
354 | ['chi', '中文 (Zhōngwén), 汉语, 漢語', true],
355 | ['rus', 'Русский', true],
356 | ['ara', 'العربية', true],
357 | ['vie', 'Việt Nam', true],
358 | ['may', 'Bahasa Melayu, بهاس ملايو', true],
359 | ['sun', 'Basa Sunda', true],
360 |
361 | // Edge cases with spaces and tabs/newlines
362 | [' chi ', '中文 (Zhōngwén), 汉语, 漢語', false],
363 | [' may ', 'bahasa Melayu, بهاس ملايو', false],
364 | ["\tchi\t", '中文 (Zhōngwén), 汉语, 漢語', false],
365 | ["\nmay\n", 'bahasa Melayu, بهاس ملايو', false],
366 |
367 | [' chi ', '中文 (Zhōngwén), 汉语, 漢語', true],
368 | [' may ', 'Bahasa Melayu, بهاس ملايو', true],
369 | ["\tchi\t", '中文 (Zhōngwén), 汉语, 漢語', true],
370 | ["\nmay\n", 'Bahasa Melayu, بهاس ملايو', true],
371 |
372 | // Edge cases with multi case
373 | ['ENG', 'English', false],
374 | ['Fre', 'français, langue française', false],
375 | ['sPA', 'español', false],
376 | ['iND', 'Bahasa Indonesia', false],
377 |
378 | ['ENG', 'English', true],
379 | ['Fre', 'Français, Langue Française', true],
380 | ['sPA', 'Español', true],
381 | ['iND', 'Bahasa Indonesia', true],
382 |
383 | // Invalid
384 | ['xxx', '', false],
385 | ['abc', '', false],
386 | ['en', '', false],
387 |
388 | ['xxx', '', true],
389 | ['abc', '', true],
390 | ['en', '', true],
391 |
392 | // Empty
393 | ['', '', false],
394 | ['', '', true],
395 | ];
396 | }
397 |
398 | /** @dataProvider nativeByCode2bDataProvider */
399 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode2bDataProvider')]
400 | public function testNativeByCode2b(string $code, string $expected, bool $isCapitalized): void
401 | {
402 | $this->assertSame($expected, $this->iso->nativeByCode2b($code, $isCapitalized));
403 | }
404 |
405 | public static function languageByCode3DataProvider(): array
406 | {
407 | return [
408 | // Happy path
409 | ['eng', 'English'],
410 | ['fra', 'French'],
411 | ['spa', 'Spanish'],
412 | ['ind', 'Indonesian'],
413 | ['jav', 'Javanese'],
414 | ['hin', 'Hindi'],
415 | ['tha', 'Thai'],
416 | ['kor', 'Korean'],
417 | ['jpn', 'Japanese'],
418 | ['zho', 'Chinese'],
419 | ['rus', 'Russian'],
420 | ['ara', 'Arabic'],
421 | ['vie', 'Vietnamese'],
422 | ['msa', 'Malay'],
423 | ['sun', 'Sundanese'],
424 |
425 | // Edge cases with spaces and tabs/newlines
426 | [' zho ', 'Chinese'],
427 | [' msa ', 'Malay'],
428 | ["\tzho\t", 'Chinese'],
429 | ["\nmsa\n", 'Malay'],
430 |
431 | // Edge cases with multi case
432 | ['ENG', 'English'],
433 | ['Fra', 'French'],
434 | ['sPA', 'Spanish'],
435 | ['iND', 'Indonesian'],
436 |
437 | // Invalid
438 | ['xxx', ''],
439 | ['abc', ''],
440 | ['en', ''],
441 |
442 | // Empty
443 | ['', ''],
444 | ];
445 | }
446 |
447 | /** @dataProvider languageByCode3DataProvider */
448 | #[\PHPUnit\Framework\Attributes\DataProvider('languageByCode3DataProvider')]
449 | public function testLanguageByCode3(string $code, string $expected): void
450 | {
451 | $this->assertSame($expected, $this->iso->languageByCode3($code));
452 | }
453 |
454 | public static function nativeByCode3DataProvider(): array
455 | {
456 | return [
457 | // Default not capitalized
458 | ['eng', 'English', false],
459 | ['fra', 'français, langue française', false],
460 | ['spa', 'español', false],
461 | ['ind', 'Bahasa Indonesia', false],
462 | ['jav', 'basa Jawa', false],
463 | ['hin', 'हिन्दी, हिंदी', false],
464 | ['tha', 'ไทย', false],
465 | ['kor', '한국어', false],
466 | ['jpn', '日本語 (にほんご)', false],
467 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', false],
468 | ['rus', 'Русский', false],
469 | ['ara', 'العربية', false],
470 | ['vie', 'Việt Nam', false],
471 | ['msa', 'bahasa Melayu, بهاس ملايو', false],
472 | ['sun', 'Basa Sunda', false],
473 |
474 | // Capitalized
475 | ['eng', 'English', true],
476 | ['fra', 'Français, Langue Française', true],
477 | ['spa', 'Español', true],
478 | ['ind', 'Bahasa Indonesia', true],
479 | ['jav', 'Basa Jawa', true],
480 | ['hin', 'हिन्दी, हिंदी', true],
481 | ['tha', 'ไทย', true],
482 | ['kor', '한국어', true],
483 | ['jpn', '日本語 (にほんご)', true],
484 | ['zho', '中文 (Zhōngwén), 汉语, 漢語', true],
485 | ['rus', 'Русский', true],
486 | ['ara', 'العربية', true],
487 | ['vie', 'Việt Nam', true],
488 | ['msa', 'Bahasa Melayu, بهاس ملايو', true],
489 | ['sun', 'Basa Sunda', true],
490 |
491 | // Edge cases with spaces and tabs/newlines
492 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', false],
493 | [' msa ', 'bahasa Melayu, بهاس ملايو', false],
494 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', false],
495 | ["\nmsa\n", 'bahasa Melayu, بهاس ملايو', false],
496 |
497 | [' zho ', '中文 (Zhōngwén), 汉语, 漢語', true],
498 | [' msa ', 'Bahasa Melayu, بهاس ملايو', true],
499 | ["\tzho\t", '中文 (Zhōngwén), 汉语, 漢語', true],
500 | ["\nmsa\n", 'Bahasa Melayu, بهاس ملايو', true],
501 |
502 | // Edge cases with multi case
503 | ['ENG', 'English', false],
504 | ['Fra', 'français, langue française', false],
505 | ['sPA', 'español', false],
506 | ['iND', 'Bahasa Indonesia', false],
507 |
508 | ['ENG', 'English', true],
509 | ['Fra', 'Français, Langue Française', true],
510 | ['sPA', 'Español', true],
511 | ['iND', 'Bahasa Indonesia', true],
512 |
513 | // Invalid
514 | ['xxx', '', false],
515 | ['abc', '', false],
516 | ['en', '', false],
517 |
518 | ['xxx', '', true],
519 | ['abc', '', true],
520 | ['en', '', true],
521 |
522 | // Empty
523 | ['', '', false],
524 | ['', '', true],
525 | ];
526 | }
527 |
528 | /** @dataProvider nativeByCode3DataProvider */
529 | #[\PHPUnit\Framework\Attributes\DataProvider('nativeByCode3DataProvider')]
530 | public function testNativeByCode3(string $code, string $expected, bool $isCapitalized): void
531 | {
532 | $this->assertSame($expected, $this->iso->nativeByCode3($code, $isCapitalized));
533 | }
534 |
535 | public static function code1ByLanguageDataProvider(): array
536 | {
537 | return [
538 | // Happy path
539 | ['en', 'English'],
540 | ['fr', 'French'],
541 | ['es', 'Spanish'],
542 | ['id', 'Indonesian'],
543 | ['jv', 'Javanese'],
544 | ['hi', 'Hindi'],
545 | ['th', 'Thai'],
546 | ['ko', 'Korean'],
547 | ['ja', 'Japanese'],
548 | ['zh', 'Chinese'],
549 | ['ru', 'Russian'],
550 | ['ar', 'Arabic'],
551 | ['vi', 'Vietnamese'],
552 | ['ms', 'Malay'],
553 | ['su', 'Sundanese'],
554 |
555 | // Edge cases with leading/trailing spaces and tabs/newlines
556 | ['zh', ' Chinese '],
557 | ['ms', ' Malay '],
558 | ['zh', "\tChinese\t"],
559 | ['ms', "\nMalay\n"],
560 |
561 | // Edge cases with multi case
562 | ['en', 'ENGLISH'],
563 | ['fr', 'FRench'],
564 |
565 | // Invalid
566 | ['', ''],
567 | ['', 'UnknownLanguage'],
568 | ['', 'Eng'],
569 |
570 | // Empty
571 | ['', ''],
572 | ];
573 | }
574 | /** @dataProvider code1ByLanguageDataProvider */
575 | #[\PHPUnit\Framework\Attributes\DataProvider('code1ByLanguageDataProvider')]
576 | public function testCode1ByLanguage(string $expected, string $language): void
577 | {
578 | $this->assertSame($expected, $this->iso->code1ByLanguage($language));
579 | }
580 |
581 | public static function code2tByLanguageDataProvider(): array
582 | {
583 | return [
584 | // Happy path
585 | ['eng', 'English'],
586 | ['fra', 'French'],
587 | ['spa', 'Spanish'],
588 | ['ind', 'Indonesian'],
589 | ['jav', 'Javanese'],
590 | ['hin', 'Hindi'],
591 | ['tha', 'Thai'],
592 | ['kor', 'Korean'],
593 | ['jpn', 'Japanese'],
594 | ['zho', 'Chinese'],
595 | ['rus', 'Russian'],
596 | ['ara', 'Arabic'],
597 | ['vie', 'Vietnamese'],
598 | ['msa', 'Malay'],
599 | ['sun', 'Sundanese'],
600 |
601 | // Edge cases with leading/trailing spaces and tabs/newlines
602 | ['zho', ' Chinese '],
603 | ['msa', ' Malay '],
604 | ['zho', "\tChinese\t"],
605 | ['msa', "\nMalay\n"],
606 |
607 | // Edge cases with multi case
608 | ['eng', 'ENGLISH'],
609 | ['fra', 'FRench'],
610 |
611 | // Invalid
612 | ['', ''],
613 | ['', 'UnknownLanguage'],
614 | ['', 'Eng'],
615 |
616 | // Empty
617 | ['', ''],
618 | ];
619 | }
620 |
621 | /** @dataProvider code2tByLanguageDataProvider */
622 | #[\PHPUnit\Framework\Attributes\DataProvider('code2tByLanguageDataProvider')]
623 | public function testCode2tByLanguage(string $expected, string $language): void
624 | {
625 | $this->assertSame($expected, $this->iso->code2tByLanguage($language));
626 | }
627 |
628 | public static function code2bByLanguageDataProvider(): array
629 | {
630 | return [
631 | // Happy path
632 | ['eng', 'English'],
633 | ['fre', 'French'],
634 | ['spa', 'Spanish'],
635 | ['ind', 'Indonesian'],
636 | ['jav', 'Javanese'],
637 | ['hin', 'Hindi'],
638 | ['tha', 'Thai'],
639 | ['kor', 'Korean'],
640 | ['jpn', 'Japanese'],
641 | ['chi', 'Chinese'],
642 | ['rus', 'Russian'],
643 | ['ara', 'Arabic'],
644 | ['vie', 'Vietnamese'],
645 | ['may', 'Malay'],
646 | ['sun', 'Sundanese'],
647 |
648 | // Edge cases with leading/trailing spaces and tabs/newlines
649 | ['chi', ' Chinese '],
650 | ['may', ' Malay '],
651 | ['chi', "\tChinese\t"],
652 | ['may', "\nMalay\n"],
653 |
654 | // Edge cases with multi case
655 | ['eng', 'ENGLISH'],
656 | ['fre', 'FRench'],
657 |
658 | // Invalid
659 | ['', ''],
660 | ['', 'UnknownLanguage'],
661 | ['', 'Eng'],
662 |
663 | // Empty
664 | ['', ''],
665 | ];
666 | }
667 |
668 | /** @dataProvider code2bByLanguageDataProvider */
669 | #[\PHPUnit\Framework\Attributes\DataProvider('code2bByLanguageDataProvider')]
670 | public function testCode2bByLanguage(string $expected, string $language): void
671 | {
672 | $this->assertSame($expected, $this->iso->code2bByLanguage($language));
673 | }
674 |
675 | public static function code3ByLanguageDataProvider(): array
676 | {
677 | return [
678 | // Happy path
679 | ['eng', 'English'],
680 | ['fra', 'French'],
681 | ['spa', 'Spanish'],
682 | ['ind', 'Indonesian'],
683 | ['jav', 'Javanese'],
684 | ['hin', 'Hindi'],
685 | ['tha', 'Thai'],
686 | ['kor', 'Korean'],
687 | ['jpn', 'Japanese'],
688 | ['zho', 'Chinese'],
689 | ['rus', 'Russian'],
690 | ['ara', 'Arabic'],
691 | ['vie', 'Vietnamese'],
692 | ['msa', 'Malay'],
693 | ['sun', 'Sundanese'],
694 |
695 | // Edge cases with leading/trailing spaces and tabs/newlines
696 | ['zho', ' Chinese '],
697 | ['msa', ' Malay '],
698 | ['zho', "\tChinese\t"],
699 | ['msa', "\nMalay\n"],
700 |
701 | // Edge cases with multi case
702 | ['eng', 'ENGLISH'],
703 | ['fra', 'FRench'],
704 |
705 | // Invalid
706 | ['', ''],
707 | ['', 'UnknownLanguage'],
708 | ['', 'Eng'],
709 |
710 | // Empty
711 | ['', ''],
712 | ];
713 | }
714 |
715 | /** @dataProvider code3ByLanguageDataProvider */
716 | #[\PHPUnit\Framework\Attributes\DataProvider('code3ByLanguageDataProvider')]
717 | public function testCode3ByLanguage(string $expected, string $language): void
718 | {
719 | $this->assertSame($expected, $this->iso->code3ByLanguage($language));
720 | }
721 |
722 | public static function getLanguageByIsoCode2bDataProvider(): array
723 | {
724 | return [
725 | [['en', 'eng', 'eng', 'eng', 'English', 'English'], 'eng'],
726 | [['fr', 'fra', 'fre', 'fra', 'French', 'français, langue française'], 'fre'],
727 | [['id', 'ind', 'ind', 'ind', 'Indonesian', 'Bahasa Indonesia'], 'ind'],
728 | ];
729 | }
730 |
731 | /** @dataProvider getLanguageByIsoCode2bDataProvider */
732 | #[\PHPUnit\Framework\Attributes\DataProvider('getLanguageByIsoCode2bDataProvider')]
733 | public function testGetLanguageByIsoCode2B(array $expected, string $code): void
734 | {
735 | $this->assertSame($expected, $this->iso->getLanguageByIsoCode2b($code));
736 | }
737 |
738 | public static function getLanguageByIsoCode2bNullDataProvider(): array
739 | {
740 | return [
741 | ['null'],
742 | ['abc'],
743 | ];
744 | }
745 |
746 | /** @dataProvider getLanguageByIsoCode2bNullDataProvider */
747 | #[\PHPUnit\Framework\Attributes\DataProvider('getLanguageByIsoCode2bNullDataProvider')]
748 | public function testGetLanguageByIsoCode2bNull(string $code): void
749 | {
750 | $this->assertNull($this->iso->getLanguageByIsoCode2b($code));
751 | }
752 |
753 | public static function code2tByCode1DataProvider(): array
754 | {
755 | return [
756 | ['fra', 'fr'],
757 | ['eng', 'en'],
758 | ['spa', 'es'],
759 | ['ind', 'id'],
760 | ];
761 | }
762 |
763 | /** @dataProvider code2tByCode1DataProvider */
764 | #[\PHPUnit\Framework\Attributes\DataProvider('code2tByCode1DataProvider')]
765 | public function testCode2tByCode1(string $expected, string $code): void
766 | {
767 | $this->assertSame($expected, $this->iso->code2tByCode1($code));
768 | }
769 |
770 | // Test allLanguages method
771 | public function testAllLanguages(): void
772 | {
773 | $languages = $this->iso->allLanguages();
774 | $this->assertIsArray($languages);
775 | $this->assertNotEmpty($languages);
776 |
777 | // Check that each language entry has 6 elements (ISO-639-1, ISO-639-2t, ISO-639-2b, ISO-639-3, English, Native)
778 | foreach ($languages as $language) {
779 | $this->assertIsArray($language);
780 | $this->assertCount(6, $language);
781 | }
782 |
783 | // Test that it contains some expected languages
784 | $englishFound = false;
785 | $frenchFound = false;
786 |
787 | foreach ($languages as $language) {
788 | if ($language[0] === 'en' && $language[4] === 'English') {
789 | $englishFound = true;
790 | }
791 | if ($language[0] === 'fr' && $language[4] === 'French') {
792 | $frenchFound = true;
793 | }
794 | }
795 |
796 | $this->assertTrue($englishFound, 'English language should be found in the languages array');
797 | $this->assertTrue($frenchFound, 'French language should be found in the languages array');
798 | }
799 |
800 | public static function consistencyDataProvider(): array
801 | {
802 | return [
803 | ['en', 'eng', 'eng', 'eng', 'English'],
804 | ['fr', 'fra', 'fre', 'fra', 'French'],
805 | ['es', 'spa', 'spa', 'spa', 'Spanish'],
806 | ['id', 'ind', 'ind', 'ind', 'Indonesian'],
807 | ['de', 'deu', 'ger', 'deu', 'German'],
808 | ];
809 | }
810 |
811 | /** @dataProvider consistencyDataProvider */
812 | #[\PHPUnit\Framework\Attributes\DataProvider('consistencyDataProvider')]
813 | public function testConsistencyBetweenCodeFormats(string $code1, string $code2t, string $code2b, string $code3, string $expectedEnglish): void
814 | {
815 | // Test that all code formats return the same English name
816 | $this->assertSame($expectedEnglish, $this->iso->languageByCode1($code1));
817 | $this->assertSame($expectedEnglish, $this->iso->languageByCode2t($code2t));
818 | $this->assertSame($expectedEnglish, $this->iso->languageByCode2b($code2b));
819 | $this->assertSame($expectedEnglish, $this->iso->languageByCode3($code3));
820 |
821 | // Test reverse lookups
822 | $this->assertSame($code1, $this->iso->code1ByLanguage($expectedEnglish));
823 | $this->assertSame($code2t, $this->iso->code2tByLanguage($expectedEnglish));
824 | $this->assertSame($code2b, $this->iso->code2bByLanguage($expectedEnglish));
825 | $this->assertSame($code3, $this->iso->code3ByLanguage($expectedEnglish));
826 |
827 | // Test code conversions
828 | $this->assertSame($code2t, $this->iso->code2tByCode1($code1));
829 | }
830 |
831 | public function testSpecialCases(): void
832 | {
833 | // Ladin language only has ISO 639-3 code
834 | $this->assertSame('lld', $this->iso->code3ByLanguage('Ladin'));
835 | $this->assertSame('Ladin', $this->iso->languageByCode3('lld'));
836 | }
837 |
838 | }
839 |
--------------------------------------------------------------------------------